oicq-icalingua-plus-plus 1.26.1 → 1.26.3
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/index.d.ts +3 -0
- package/lib/client-ext.js +181 -243
- package/lib/client.js +8 -2
- package/lib/device.js +17 -0
- package/lib/message/face.js +1 -0
- package/lib/message/file.js +10 -1
- package/lib/message/image.js +32 -104
- package/lib/message/parser.js +3 -3
- package/lib/oicq.js +8 -0
- package/lib/qsign-api.js +158 -0
- package/lib/ref.d.ts +2 -0
- package/lib/wtlogin/login-password.js +6 -0
- package/lib/wtlogin/tlv.js +1 -1
- package/lib/wtlogin/wt.js +9 -5
- package/package.json +17 -17
package/index.d.ts
CHANGED
|
@@ -1165,6 +1165,9 @@ export class Client extends EventEmitter {
|
|
|
1165
1165
|
/** 踢出已登录的设备 */
|
|
1166
1166
|
delDevLoginInfo(flag: string): Promise<Ret>;
|
|
1167
1167
|
|
|
1168
|
+
/** 根据fileid获取私聊文件URL */
|
|
1169
|
+
getC2CFileUrl(fileid: string): Promise<Ret<string>>;
|
|
1170
|
+
|
|
1168
1171
|
/** 清除 image 和 record 文件夹下的缓存文件 */
|
|
1169
1172
|
cleanCache(type?: "image" | "record"): Promise<Ret>;
|
|
1170
1173
|
|
package/lib/client-ext.js
CHANGED
|
@@ -11,19 +11,7 @@ const { timestamp, uin2code, BUF16, BUF0, log } = require("./common");
|
|
|
11
11
|
const { TimeoutError } = require("./exception");
|
|
12
12
|
const { parseC2CMsg } = require("./message/parser");
|
|
13
13
|
const Writer = require("./wtlogin/writer");
|
|
14
|
-
const
|
|
15
|
-
const http = require("http");
|
|
16
|
-
const https = require("https");
|
|
17
|
-
const API_WAIT_TIME = 45000;
|
|
18
|
-
const axios = _axios.create({
|
|
19
|
-
httpAgent: new http.Agent({ keepAlive: true }),
|
|
20
|
-
httpsAgent: new https.Agent({ keepAlive: true }),
|
|
21
|
-
timeout: API_WAIT_TIME,
|
|
22
|
-
headers: {
|
|
23
|
-
'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.199 Safari/537.36 OICQ/1.23 ILPP/2",
|
|
24
|
-
'Content-Type': "application/x-www-form-urlencoded"
|
|
25
|
-
}
|
|
26
|
-
});
|
|
14
|
+
const { buildUrl, getCommonParams, qsignGet, qsignPost, executeWithRetry, sleep } = require("./qsign-api");
|
|
27
15
|
|
|
28
16
|
/**
|
|
29
17
|
* seqid递增并返回
|
|
@@ -377,10 +365,6 @@ Client.prototype.pbGetMsg = async function (force = false, flag = 0, count = 10)
|
|
|
377
365
|
}
|
|
378
366
|
};
|
|
379
367
|
|
|
380
|
-
function sleep(time) {
|
|
381
|
-
return new Promise(resolve => setTimeout(resolve, time));
|
|
382
|
-
}
|
|
383
|
-
|
|
384
368
|
class Lock {
|
|
385
369
|
constructor() {
|
|
386
370
|
this.queue = [];
|
|
@@ -409,37 +393,22 @@ const requestLock = new Lock();
|
|
|
409
393
|
|
|
410
394
|
/**
|
|
411
395
|
* 注册qsign
|
|
396
|
+
* @param {boolean} [lockAcquired=false] 调用者是否已持有requestLock
|
|
412
397
|
*/
|
|
413
|
-
Client.prototype.registerQSign = async function(
|
|
398
|
+
Client.prototype.registerQSign = async function(lockAcquired = false) {
|
|
414
399
|
if (!this.config.sign_api_addr) return BUF0;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
};
|
|
428
|
-
await requestLock.acquire();
|
|
429
|
-
const { data } = await axios.get(url,
|
|
430
|
-
{
|
|
431
|
-
params,
|
|
432
|
-
}
|
|
433
|
-
).catch((e) => ({ data: { code: -1, msg: e ? e.message : 'failed to connect to qsign api' } }));
|
|
434
|
-
requestLock.release();
|
|
435
|
-
if (data.code !== 0) {
|
|
436
|
-
if (retry < 3) {
|
|
437
|
-
this.logger.warn(`[qsign][register] ${data.msg}(${data.code}), retry in 1s...`);
|
|
438
|
-
await sleep(1000);
|
|
439
|
-
return await this.registerQSign(retry + 1);
|
|
440
|
-
}
|
|
441
|
-
data.msg = data.msg || 'unknown error';
|
|
442
|
-
throw new Error(`[qsign][register] ${data.msg}(${data.code})`);
|
|
400
|
+
const url = buildUrl(this.config.sign_api_addr, 'register');
|
|
401
|
+
const params = getCommonParams.call(this);
|
|
402
|
+
if (!lockAcquired) await requestLock.acquire();
|
|
403
|
+
try {
|
|
404
|
+
const data = await executeWithRetry(
|
|
405
|
+
() => qsignGet(url, params),
|
|
406
|
+
'register',
|
|
407
|
+
{ throwOnError: true, logger: this.logger }
|
|
408
|
+
);
|
|
409
|
+
return data;
|
|
410
|
+
} finally {
|
|
411
|
+
if (!lockAcquired) requestLock.release();
|
|
443
412
|
}
|
|
444
413
|
};
|
|
445
414
|
|
|
@@ -469,89 +438,50 @@ Client.prototype.handleSsoCallback = async function(callbackList = []) {
|
|
|
469
438
|
* @param {number} callbackId
|
|
470
439
|
* @param {Buffer} body
|
|
471
440
|
*/
|
|
472
|
-
Client.prototype.submitSsoCallback = async function(cmd, callbackId, body
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
url += '/';
|
|
476
|
-
}
|
|
477
|
-
url += 'submit';
|
|
478
|
-
const params = {
|
|
479
|
-
ver: this.apk.ver,
|
|
480
|
-
android_id: this.device.android_id,
|
|
481
|
-
androidId: this.device.android_id,
|
|
482
|
-
qimei36: this.device.qimei36 || this.device.qimei16 || this.device.android_id,
|
|
483
|
-
guid: this.device.guid.toString('hex'),
|
|
484
|
-
uin: this.uin,
|
|
441
|
+
Client.prototype.submitSsoCallback = async function(cmd, callbackId, body) {
|
|
442
|
+
const url = buildUrl(this.config.sign_api_addr, 'submit');
|
|
443
|
+
const params = getCommonParams.call(this, {
|
|
485
444
|
buffer: body.toString('hex'),
|
|
486
445
|
cmd: cmd,
|
|
487
446
|
callback_id: callbackId,
|
|
488
447
|
callbackId: callbackId,
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
{
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
).catch((e) => ({ data: { code: -1, msg: e ? e.message : 'failed to connect to qsign api' } }));
|
|
497
|
-
if (data.code !== 0) {
|
|
498
|
-
if (retry < 3) {
|
|
499
|
-
this.logger.warn(`[qsign][submitSso] ${data.msg}(${data.code}), retry in 1s...`);
|
|
500
|
-
await sleep(1000);
|
|
501
|
-
return await this.submitSsoCallback(cmd, callbackId, body, retry + 1);
|
|
502
|
-
}
|
|
503
|
-
data.msg = data.msg || 'unknown error';
|
|
504
|
-
this.logger.warn(`[qsign][submitSso] ${data.msg}(${data.code})`);
|
|
505
|
-
}
|
|
448
|
+
});
|
|
449
|
+
await executeWithRetry(
|
|
450
|
+
() => qsignGet(url, params),
|
|
451
|
+
'submitSso',
|
|
452
|
+
{ throwOnError: false, logger: this.logger }
|
|
453
|
+
);
|
|
506
454
|
}
|
|
507
455
|
|
|
508
456
|
/**
|
|
509
457
|
* 刷新qsign的token
|
|
510
458
|
*/
|
|
511
|
-
Client.prototype.refreshQSignToken = async function(
|
|
459
|
+
Client.prototype.refreshQSignToken = async function() {
|
|
512
460
|
if (!this.config.sign_api_addr) return;
|
|
513
461
|
if (unsendSso.length > 0) {
|
|
514
462
|
await this.handleSsoCallback(unsendSso.splice(0, unsendSso.length));
|
|
515
463
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const params = {
|
|
522
|
-
ver: this.apk.ver,
|
|
523
|
-
uin: this.uin,
|
|
524
|
-
android_id: this.device.android_id,
|
|
525
|
-
androidId: this.device.android_id,
|
|
526
|
-
qimei36: this.device.qimei36 || this.device.qimei16 || this.device.android_id,
|
|
527
|
-
guid: this.device.guid.toString('hex'),
|
|
528
|
-
qua: this.apk.qua,
|
|
529
|
-
};
|
|
530
|
-
const { data } = await axios.get(url,
|
|
464
|
+
const url = buildUrl(this.config.sign_api_addr, 'request_token');
|
|
465
|
+
const params = getCommonParams.call(this);
|
|
466
|
+
const data = await executeWithRetry(
|
|
467
|
+
() => qsignGet(url, params),
|
|
468
|
+
'refreshToken',
|
|
531
469
|
{
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return await this.refreshQSignToken(retry);
|
|
545
|
-
}
|
|
546
|
-
if (retry < 3) {
|
|
547
|
-
this.logger.warn(`[qsign][refreshToken] ${data.msg}(${data.code}), retry in 1s...`);
|
|
548
|
-
await sleep(1000);
|
|
549
|
-
return await this.refreshQSignToken(retry + 1);
|
|
470
|
+
throwOnError: false,
|
|
471
|
+
handleNotRegistered: true,
|
|
472
|
+
onNotRegistered: async () => {
|
|
473
|
+
try {
|
|
474
|
+
await this.registerQSign();
|
|
475
|
+
} catch (e) {
|
|
476
|
+
this.logger.warn(`${e.message || '[qsign][register] unknown error'}`);
|
|
477
|
+
console.warn(e);
|
|
478
|
+
}
|
|
479
|
+
await sleep(100);
|
|
480
|
+
},
|
|
481
|
+
logger: this.logger,
|
|
550
482
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
if (data.data){
|
|
483
|
+
);
|
|
484
|
+
if (data.data) {
|
|
555
485
|
const pkts = data.data.ssoPacketList || data.data.requestCallback || data.data;
|
|
556
486
|
if (Array.isArray(pkts)) {
|
|
557
487
|
await this.handleSsoCallback(pkts);
|
|
@@ -575,7 +505,7 @@ Client.prototype.buildSSOReserveField = async function(cmd, body, seq_id = this.
|
|
|
575
505
|
12: qimei, //qimei (string)
|
|
576
506
|
14: 0, //newconn_flag (uint32)
|
|
577
507
|
15: null, //trace_parent (string)
|
|
578
|
-
16: String(this.
|
|
508
|
+
16: this.uid ? String(this.uid) : "", //uid (string)
|
|
579
509
|
18: 0, //imsi (uint32)
|
|
580
510
|
19: 1, //network_type (uint32)
|
|
581
511
|
20: 1, //ip_stack_type (uint32)
|
|
@@ -596,7 +526,6 @@ Client.prototype.buildSSOReserveField = async function(cmd, body, seq_id = this.
|
|
|
596
526
|
}
|
|
597
527
|
}
|
|
598
528
|
if (options.nt_core) { //此时数据包内需用uid取代uin
|
|
599
|
-
reserveFields[16] = String(this.uid);
|
|
600
529
|
reserveFields[21] |= 32;
|
|
601
530
|
reserveFields[26] = 100;
|
|
602
531
|
}
|
|
@@ -611,117 +540,88 @@ Client.prototype.buildSSOReserveField = async function(cmd, body, seq_id = this.
|
|
|
611
540
|
* @param {string} cmd
|
|
612
541
|
* @param {Buffer} body
|
|
613
542
|
* @param {number} seq_id
|
|
614
|
-
* @param {number} retry 重试次数
|
|
615
543
|
*/
|
|
616
|
-
Client.prototype.getSign = async function(cmd, body, seq_id = this.seq_id
|
|
544
|
+
Client.prototype.getSign = async function(cmd, body, seq_id = this.seq_id) {
|
|
617
545
|
if (!this.config.sign_api_addr || !this.apk.qua) return BUF0;
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
546
|
+
const url = buildUrl(this.config.sign_api_addr, 'sign');
|
|
547
|
+
const params = getCommonParams.call(this, {
|
|
548
|
+
buffer: body.toString('hex'),
|
|
549
|
+
cmd: cmd,
|
|
550
|
+
seq: seq_id,
|
|
551
|
+
});
|
|
623
552
|
await requestLock.acquire();
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
},
|
|
638
|
-
).catch((e) => ({ data: { code: -1, msg: e ? e.message : 'failed to connect to qsign api' } }));
|
|
639
|
-
requestLock.release();
|
|
640
|
-
if (data.code !== 0) {
|
|
641
|
-
if (data.code === 1 && String(data.msg).includes('not registered')) {
|
|
642
|
-
await this.registerQSign();
|
|
643
|
-
await sleep(1000);
|
|
644
|
-
return await this.getSign(cmd, body, seq_id, retry);
|
|
645
|
-
}
|
|
646
|
-
if (retry < 3) {
|
|
647
|
-
this.logger.warn(`[qsign][HeadSign] ${data.msg}(${data.code}), retry in 1s...`);
|
|
648
|
-
await sleep(1000);
|
|
649
|
-
return await this.getSign(cmd, body, seq_id, retry + 1);
|
|
553
|
+
try {
|
|
554
|
+
const data = await executeWithRetry(
|
|
555
|
+
() => qsignPost(url, params),
|
|
556
|
+
'HeadSign',
|
|
557
|
+
{
|
|
558
|
+
throwOnError: true,
|
|
559
|
+
handleNotRegistered: true,
|
|
560
|
+
onNotRegistered: () => this.registerQSign(true),
|
|
561
|
+
logger: this.logger,
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
if (!data.data.sign) {
|
|
565
|
+
throw new Error(`[qsign][HeadSign] server return invalid response: ${JSON.stringify(data)}`);
|
|
650
566
|
}
|
|
651
|
-
data.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
setTimeout(() => this.refreshQSignToken(), 100);
|
|
662
|
-
} else {
|
|
663
|
-
this.once("internal.registered", async () => {
|
|
664
|
-
await this.refreshQSignToken();
|
|
665
|
-
});
|
|
567
|
+
if (data.data.requestCallback && data.data.requestCallback.length > 0) {
|
|
568
|
+
// 成功上线register后再发送SSO包
|
|
569
|
+
unsendSso.push.apply(unsendSso, data.data.requestCallback);
|
|
570
|
+
if (this.isOnline()) {
|
|
571
|
+
setTimeout(() => this.refreshQSignToken(), 100);
|
|
572
|
+
} else {
|
|
573
|
+
this.once("internal.registered", async () => {
|
|
574
|
+
await this.refreshQSignToken();
|
|
575
|
+
});
|
|
576
|
+
}
|
|
666
577
|
}
|
|
578
|
+
return Buffer.from(pb.encode({
|
|
579
|
+
1: Buffer.from(data.data.sign, "hex"),
|
|
580
|
+
2: Buffer.from(data.data.token, "hex"),
|
|
581
|
+
3: Buffer.from(data.data.extra, "hex"),
|
|
582
|
+
}));
|
|
583
|
+
} finally {
|
|
584
|
+
requestLock.release();
|
|
667
585
|
}
|
|
668
|
-
return Buffer.from(pb.encode({
|
|
669
|
-
1: Buffer.from(data.data.sign, "hex"),
|
|
670
|
-
2: Buffer.from(data.data.token, "hex"),
|
|
671
|
-
3: Buffer.from(data.data.extra, "hex"),
|
|
672
|
-
}));
|
|
673
586
|
};
|
|
674
587
|
|
|
675
588
|
/**
|
|
676
589
|
* 获取Tlv544
|
|
677
590
|
* @param {string} cmd
|
|
678
|
-
* @param {number} retry 重试次数
|
|
679
591
|
*/
|
|
680
|
-
Client.prototype.getT544 = async function(cmd
|
|
592
|
+
Client.prototype.getT544 = async function(cmd) {
|
|
681
593
|
if (!this.config.sign_api_addr || !this.apk.qua) return BUF0;
|
|
682
594
|
if (this.config.force_algo_T544) return BUF0;
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
595
|
+
// 优先使用custom_energy,避免服务器使用不正确的 salt 组装
|
|
596
|
+
try {
|
|
597
|
+
return await this.getT544CustomEnergy(cmd);
|
|
598
|
+
} catch (e) {
|
|
599
|
+
this.logger.warn(`[qsign][T544] /custom_energy failed, falling back to /energy: ${e.message || e}`);
|
|
686
600
|
}
|
|
687
|
-
url
|
|
688
|
-
const params = {
|
|
689
|
-
ver: this.apk.ver,
|
|
690
|
-
androidId: this.device.android_id,
|
|
691
|
-
android_id: this.device.android_id,
|
|
692
|
-
qimei36: this.device.qimei36 || this.device.qimei16 || this.device.android_id,
|
|
693
|
-
guid: this.device.guid.toString('hex'),
|
|
694
|
-
uin: this.uin,
|
|
601
|
+
const url = buildUrl(this.config.sign_api_addr, 'energy');
|
|
602
|
+
const params = getCommonParams.call(this, {
|
|
695
603
|
data: cmd,
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
604
|
+
version: this.apk.sdkver,
|
|
605
|
+
});
|
|
606
|
+
const data = await executeWithRetry(
|
|
607
|
+
() => qsignGet(url, params),
|
|
608
|
+
'T544',
|
|
700
609
|
{
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (data.code === 1 && String(data.msg).includes('not registered')) {
|
|
706
|
-
await this.registerQSign();
|
|
707
|
-
await sleep(1000);
|
|
708
|
-
return await this.getT544(cmd, retry);
|
|
610
|
+
throwOnError: true,
|
|
611
|
+
handleNotRegistered: true,
|
|
612
|
+
onNotRegistered: () => this.registerQSign(),
|
|
613
|
+
logger: this.logger,
|
|
709
614
|
}
|
|
710
|
-
|
|
711
|
-
this.logger.warn(`[qsign][T544] ${data.msg}(${data.code}), retry in 1s...`);
|
|
712
|
-
await sleep(1000);
|
|
713
|
-
return await this.getT544(cmd, retry + 1);
|
|
714
|
-
}
|
|
715
|
-
data.msg = data.msg || 'unknown error';
|
|
716
|
-
throw new Error(`[qsign][T544] ${data.msg}(${data.code})`);
|
|
717
|
-
}
|
|
615
|
+
);
|
|
718
616
|
if (!data.data) {
|
|
719
|
-
|
|
720
|
-
return await this.getT544CustomEnergy(cmd);
|
|
617
|
+
throw new Error(`[qsign][T544] server return invalid response: ${JSON.stringify(data)}`);
|
|
721
618
|
}
|
|
722
|
-
if (typeof(data.data) === 'string')
|
|
619
|
+
if (typeof(data.data) === 'string') {
|
|
620
|
+
if (this.sig.t553 && this.sig.t553.length === 0) {
|
|
621
|
+
await this.getXwDebugId();
|
|
622
|
+
}
|
|
723
623
|
return Buffer.from(data.data, 'hex');
|
|
724
|
-
else if (typeof(data.data.sign) === 'string') {
|
|
624
|
+
} else if (typeof(data.data.sign) === 'string') {
|
|
725
625
|
if (typeof(data.data.t553) === 'string') this.sig.t553 = Buffer.from(data.data.t553, 'hex');
|
|
726
626
|
return Buffer.from(data.data.sign, 'hex');
|
|
727
627
|
}
|
|
@@ -731,20 +631,15 @@ Client.prototype.getT544 = async function(cmd, retry = 0) {
|
|
|
731
631
|
/**
|
|
732
632
|
* 获取Tlv544 custom_energy
|
|
733
633
|
* @param {string} cmd
|
|
734
|
-
* @param {number} retry 重试次数
|
|
735
634
|
*/
|
|
736
|
-
Client.prototype.getT544CustomEnergy = async function(cmd
|
|
635
|
+
Client.prototype.getT544CustomEnergy = async function(cmd) {
|
|
737
636
|
if (!this.config.sign_api_addr || !this.apk.qua) return BUF0;
|
|
738
637
|
if (this.config.force_algo_T544) return BUF0;
|
|
739
|
-
|
|
740
|
-
if (url[url.length - 1] !== '/') {
|
|
741
|
-
url += '/';
|
|
742
|
-
}
|
|
743
|
-
url += 'custom_energy';
|
|
638
|
+
const url = buildUrl(this.config.sign_api_addr, 'custom_energy');
|
|
744
639
|
const salt = new Writer();
|
|
745
640
|
const subCmd = parseInt('0x' + (cmd.split("_")[1] || 9));
|
|
746
641
|
if (["810_9", "810_a", "810_d", "810_f"].includes(cmd)) {
|
|
747
|
-
salt.writeU32(0)
|
|
642
|
+
salt.writeU32(this.uin > 2147483647 ? 0xffffffff : 0)
|
|
748
643
|
salt.writeTlv(this.device.guid)
|
|
749
644
|
salt.writeTlv(this.apk.sdkver)
|
|
750
645
|
salt.writeU32(subCmd)
|
|
@@ -755,44 +650,87 @@ Client.prototype.getT544CustomEnergy = async function(cmd, retry = 0) {
|
|
|
755
650
|
salt.writeTlv(this.apk.sdkver)
|
|
756
651
|
salt.writeU32(subCmd)
|
|
757
652
|
}
|
|
758
|
-
const params = {
|
|
759
|
-
ver: this.apk.ver,
|
|
760
|
-
androidId: this.device.android_id,
|
|
761
|
-
android_id: this.device.android_id,
|
|
762
|
-
qimei36: this.device.qimei36 || this.device.qimei16 || this.device.android_id,
|
|
763
|
-
guid: this.device.guid.toString('hex'),
|
|
764
|
-
uin: this.uin,
|
|
653
|
+
const params = getCommonParams.call(this, {
|
|
765
654
|
data: cmd,
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
655
|
+
salt: salt.read().toString('hex'),
|
|
656
|
+
});
|
|
657
|
+
const data = await executeWithRetry(
|
|
658
|
+
() => qsignGet(url, params),
|
|
659
|
+
'T544',
|
|
770
660
|
{
|
|
771
|
-
|
|
661
|
+
throwOnError: true,
|
|
662
|
+
handleNotRegistered: true,
|
|
663
|
+
onNotRegistered: () => this.registerQSign(),
|
|
664
|
+
logger: this.logger,
|
|
772
665
|
}
|
|
773
|
-
)
|
|
774
|
-
if (data.code !== 0) {
|
|
775
|
-
if (data.code === 1 && String(data.msg).includes('not registered')) {
|
|
776
|
-
await this.registerQSign();
|
|
777
|
-
await sleep(1000);
|
|
778
|
-
return await this.getT544CustomEnergy(cmd, retry);
|
|
779
|
-
}
|
|
780
|
-
if (retry < 3) {
|
|
781
|
-
this.logger.warn(`[qsign][T544] ${data.msg}(${data.code}), retry in 1s...`);
|
|
782
|
-
await sleep(1000);
|
|
783
|
-
return await this.getT544CustomEnergy(cmd, retry + 1);
|
|
784
|
-
}
|
|
785
|
-
data.msg = data.msg || 'unknown error';
|
|
786
|
-
throw new Error(`[qsign][T544] ${data.msg}(${data.code})`);
|
|
787
|
-
}
|
|
666
|
+
);
|
|
788
667
|
if (!data.data) {
|
|
789
668
|
throw new Error(`[qsign][T544] server return invalid response: ${JSON.stringify(data)}`);
|
|
790
669
|
}
|
|
791
|
-
if (typeof(data.data) === 'string')
|
|
670
|
+
if (typeof(data.data) === 'string') {
|
|
671
|
+
if (this.sig.t553 && this.sig.t553.length === 0) {
|
|
672
|
+
await this.getXwDebugId();
|
|
673
|
+
}
|
|
792
674
|
return Buffer.from(data.data, 'hex');
|
|
793
|
-
else if (typeof(data.data.sign) === 'string') {
|
|
675
|
+
} else if (typeof(data.data.sign) === 'string') {
|
|
794
676
|
if (typeof(data.data.t553) === 'string') this.sig.t553 = Buffer.from(data.data.t553, 'hex');
|
|
795
677
|
return Buffer.from(data.data.sign, 'hex');
|
|
796
678
|
}
|
|
797
679
|
else throw new Error(`[qsign][T544] server return invalid response: ${JSON.stringify(data)}`);
|
|
798
680
|
};
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* 从服务器获得XwDebugId
|
|
684
|
+
*/
|
|
685
|
+
Client.prototype.getXwDebugId = async function() {
|
|
686
|
+
if (!this.config.sign_api_addr || !this.apk.qua) return;
|
|
687
|
+
const url = buildUrl(this.config.sign_api_addr, 'get_xw_debug_id');
|
|
688
|
+
const params = getCommonParams.call(this);
|
|
689
|
+
await requestLock.acquire();
|
|
690
|
+
try {
|
|
691
|
+
const data = await executeWithRetry(
|
|
692
|
+
() => qsignGet(url, params),
|
|
693
|
+
'XwDebugId',
|
|
694
|
+
{
|
|
695
|
+
throwOnError: false,
|
|
696
|
+
handleNotRegistered: true,
|
|
697
|
+
onNotRegistered: () => this.registerQSign(true),
|
|
698
|
+
logger: this.logger,
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
if (typeof(data.data) === 'string') {
|
|
702
|
+
this.sig.t553 = Buffer.from(data.data, 'hex');
|
|
703
|
+
this.logger.info(`[qsign][XwDebugId] updated t553: ${data.data}`);
|
|
704
|
+
}
|
|
705
|
+
} finally {
|
|
706
|
+
requestLock.release();
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* 从服务器拉取签名cmd名单
|
|
712
|
+
*/
|
|
713
|
+
Client.prototype.updateCmdWhiteList = async function() {
|
|
714
|
+
if (!this.config.sign_api_addr || !this.apk.qua) return;
|
|
715
|
+
const url = buildUrl(this.config.sign_api_addr, 'cmd_whitelist');
|
|
716
|
+
const params = getCommonParams.call(this);
|
|
717
|
+
await requestLock.acquire();
|
|
718
|
+
try {
|
|
719
|
+
const data = await executeWithRetry(
|
|
720
|
+
() => qsignGet(url, params),
|
|
721
|
+
'CmdWhiteList',
|
|
722
|
+
{
|
|
723
|
+
throwOnError: false,
|
|
724
|
+
handleNotRegistered: true,
|
|
725
|
+
onNotRegistered: () => this.registerQSign(true),
|
|
726
|
+
logger: this.logger,
|
|
727
|
+
}
|
|
728
|
+
);
|
|
729
|
+
if (data.data && Array.isArray(data.data.list) && data.data.list.length > 0) {
|
|
730
|
+
this.needSignCmd = data.data.list;
|
|
731
|
+
this.logger.info(`[qsign][CmdWhiteList] updated needSignCmd list, count: ${this.needSignCmd.length}`);
|
|
732
|
+
}
|
|
733
|
+
} finally {
|
|
734
|
+
requestLock.release();
|
|
735
|
+
}
|
|
736
|
+
};
|
package/lib/client.js
CHANGED
|
@@ -22,7 +22,7 @@ const { WtLogin } = require("./wtlogin/wt");
|
|
|
22
22
|
const chat = require("./message/chat");
|
|
23
23
|
const multi = require("./message/multi");
|
|
24
24
|
const image = require("./message/image");
|
|
25
|
-
const { Gfs } = require("./message/file");
|
|
25
|
+
const { Gfs, getC2CFileUrlApi } = require("./message/file");
|
|
26
26
|
const { getErrorMessage, TimeoutError } = require("./exception");
|
|
27
27
|
|
|
28
28
|
function buildApiRet(retcode, data = null, error = null) {
|
|
@@ -161,6 +161,7 @@ class Client extends EventEmitter {
|
|
|
161
161
|
seq_id = 1145 + Math.round(Math.random() * 1419);
|
|
162
162
|
handlers = new Map; //存放响应包的回调
|
|
163
163
|
seq_cache = new Map; //一分钟内的缓存
|
|
164
|
+
_registerRetryCount = 0; //register失败重试计数
|
|
164
165
|
|
|
165
166
|
sig = {
|
|
166
167
|
srm_token: BUF0,
|
|
@@ -370,6 +371,7 @@ class Client extends EventEmitter {
|
|
|
370
371
|
else
|
|
371
372
|
this.password_md5 = md5(String(password));
|
|
372
373
|
}
|
|
374
|
+
this._registerRetryCount = 0;
|
|
373
375
|
this._connect(() => {
|
|
374
376
|
this._wt.syncTimeDiff();
|
|
375
377
|
this._wt.passwordLogin();
|
|
@@ -628,7 +630,7 @@ class Client extends EventEmitter {
|
|
|
628
630
|
return this._useProtocol(chat.preloadImages, arguments);
|
|
629
631
|
}
|
|
630
632
|
|
|
631
|
-
getNTPicURLbyFileid() {
|
|
633
|
+
getNTPicURLbyFileid(fileid) {
|
|
632
634
|
return this._useProtocol(image.getNTPicURLbyFileidApi, arguments);
|
|
633
635
|
}
|
|
634
636
|
|
|
@@ -640,6 +642,10 @@ class Client extends EventEmitter {
|
|
|
640
642
|
return this._useProtocol(nessy.delDevLoginInfo, arguments);
|
|
641
643
|
}
|
|
642
644
|
|
|
645
|
+
getC2CFileUrl(fileid) {
|
|
646
|
+
return this._useProtocol(getC2CFileUrlApi, arguments);
|
|
647
|
+
}
|
|
648
|
+
|
|
643
649
|
///////////////////////////////////////////////////
|
|
644
650
|
|
|
645
651
|
async getCookies(domain) {
|
package/lib/device.js
CHANGED
|
@@ -564,6 +564,19 @@ const apk = {
|
|
|
564
564
|
ssover: 22,
|
|
565
565
|
qua: "V1_AND_SQ_9.1.20_8198_YYB_D",
|
|
566
566
|
},
|
|
567
|
+
|
|
568
|
+
//android phone 9.2.80
|
|
569
|
+
61: {
|
|
570
|
+
...DEFAULT_ANDROID_APK_INFO,
|
|
571
|
+
name: "A9.2.80.90316290",
|
|
572
|
+
version: "9.2.80.35125",
|
|
573
|
+
ver: "9.2.80",
|
|
574
|
+
buildtime: 1762849776,
|
|
575
|
+
subid: 537348293,
|
|
576
|
+
sdkver: "6.0.0.2591",
|
|
577
|
+
ssover: 22,
|
|
578
|
+
qua: "V1_AND_SQ_9.2.80_13690_YYB_D",
|
|
579
|
+
},
|
|
567
580
|
};
|
|
568
581
|
|
|
569
582
|
//apad
|
|
@@ -674,6 +687,10 @@ apk[58].subid = 537244932;
|
|
|
674
687
|
apk[60] = { ...apk[59] };
|
|
675
688
|
apk[60].subid = 537257453;
|
|
676
689
|
|
|
690
|
+
//apad 9.2.80
|
|
691
|
+
apk[62] = { ...apk[61] };
|
|
692
|
+
apk[62].subid = 537348332;
|
|
693
|
+
|
|
677
694
|
//ipad
|
|
678
695
|
apk[5] = { ...apk[1] };
|
|
679
696
|
apk[5].subid = 537155074;
|
package/lib/message/face.js
CHANGED
package/lib/message/file.js
CHANGED
|
@@ -426,6 +426,15 @@ async function getC2CFileUrl(fileid) {
|
|
|
426
426
|
return _url.toString();
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
/**
|
|
430
|
+
* @this {import("../ref").Client}
|
|
431
|
+
* @param {Buffer|string} fileid
|
|
432
|
+
*/
|
|
433
|
+
async function getC2CFileUrlApi(fileid) {
|
|
434
|
+
const url = await getC2CFileUrl.call(this, fileid);
|
|
435
|
+
return { result: 0, data: url };
|
|
436
|
+
}
|
|
437
|
+
|
|
429
438
|
/**
|
|
430
439
|
* @this {import("../ref").Client}
|
|
431
440
|
* @param {Buffer|string} fileid
|
|
@@ -456,5 +465,5 @@ async function getSyncDeviceFileUrl(fileid) {
|
|
|
456
465
|
}
|
|
457
466
|
|
|
458
467
|
module.exports = {
|
|
459
|
-
getC2CFileUrl, getSyncDeviceFileUrl, Gfs
|
|
468
|
+
getC2CFileUrl, getC2CFileUrlApi, getSyncDeviceFileUrl, Gfs
|
|
460
469
|
};
|
package/lib/message/image.js
CHANGED
|
@@ -56,6 +56,9 @@ const rkey_cache = {
|
|
|
56
56
|
guildNTPicRkey: { value: '', expire: 0 },
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** @type {Promise<typeof rkey_cache> | null} */
|
|
60
|
+
let rkey_fetching = null;
|
|
61
|
+
|
|
59
62
|
/**
|
|
60
63
|
* 生成CQ码file字段
|
|
61
64
|
* @param {string} md5
|
|
@@ -627,91 +630,31 @@ async function checkImgUrl(url) {
|
|
|
627
630
|
}
|
|
628
631
|
|
|
629
632
|
/**
|
|
630
|
-
* 获取QQNT
|
|
631
|
-
* @this {import("../ref").Client}
|
|
632
|
-
* @param {Object} imgInfo
|
|
633
|
-
* @param {String} uid
|
|
634
|
-
*/
|
|
635
|
-
async function getOffNTPicURL(imgInfo, uid) {
|
|
636
|
-
if (!uid) uid = String(this.uid || this.uin);
|
|
637
|
-
const body = pb.encode({
|
|
638
|
-
1: {
|
|
639
|
-
1: {
|
|
640
|
-
1: 1,
|
|
641
|
-
2: 200,
|
|
642
|
-
},
|
|
643
|
-
2: {
|
|
644
|
-
101: 2,
|
|
645
|
-
102: 1,
|
|
646
|
-
200: 1,
|
|
647
|
-
201: {
|
|
648
|
-
1: 2,
|
|
649
|
-
2: uid,
|
|
650
|
-
},
|
|
651
|
-
},
|
|
652
|
-
3: {
|
|
653
|
-
1: 2,
|
|
654
|
-
},
|
|
655
|
-
},
|
|
656
|
-
3: {
|
|
657
|
-
1: imgInfo,
|
|
658
|
-
2: {
|
|
659
|
-
2: {
|
|
660
|
-
1: 0,
|
|
661
|
-
3: 0,
|
|
662
|
-
},
|
|
663
|
-
},
|
|
664
|
-
},
|
|
665
|
-
});
|
|
666
|
-
try {
|
|
667
|
-
const rsp = await this.sendOidbSvcTrpcTcp("OidbSvcTrpcTcp.0x11c5_200", body);
|
|
668
|
-
const rkey = rsp[3][1];
|
|
669
|
-
if (!rkey)
|
|
670
|
-
throw new Error("rkey不存在");
|
|
671
|
-
return `https://${rsp[3][3][1]}${rsp[3][3][2]}${rkey}&spec=0`;
|
|
672
|
-
} catch (e) {
|
|
673
|
-
this.logger.warn("获取QQNT图片URL失败", e.message);
|
|
674
|
-
return null;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* 获取QQNT群图URL
|
|
633
|
+
* 获取QQNT图片URL
|
|
680
634
|
* @this {import("../ref").Client}
|
|
681
635
|
* @param {Object} imgInfo
|
|
682
|
-
* @param {number}
|
|
636
|
+
* @param {number|string} target 群号或UID
|
|
637
|
+
* @param {boolean} c2c 是否为私聊
|
|
683
638
|
*/
|
|
684
|
-
async function
|
|
639
|
+
async function getNTPicURL(imgInfo, target, c2c = false) {
|
|
640
|
+
if (!target) target = c2c ? String(this.uid || this.uin) : 284840486;
|
|
685
641
|
const body = pb.encode({
|
|
686
642
|
1: {
|
|
687
|
-
1: {
|
|
688
|
-
1: 1,
|
|
689
|
-
2: 200,
|
|
690
|
-
},
|
|
643
|
+
1: { 1: 1, 2: 200 },
|
|
691
644
|
2: {
|
|
692
|
-
101: 2,
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
202: {
|
|
696
|
-
1: gid,
|
|
697
|
-
},
|
|
698
|
-
},
|
|
699
|
-
3: {
|
|
700
|
-
1: 2,
|
|
645
|
+
101: 2, 102: 1,
|
|
646
|
+
200: c2c ? 1 : 2,
|
|
647
|
+
[c2c ? 201 : 202]: c2c ? { 1: 2, 2: String(target) } : { 1: target },
|
|
701
648
|
},
|
|
649
|
+
3: { 1: 2 },
|
|
702
650
|
},
|
|
703
651
|
3: {
|
|
704
652
|
1: imgInfo,
|
|
705
|
-
2: {
|
|
706
|
-
2: {
|
|
707
|
-
1: 0,
|
|
708
|
-
3: 0,
|
|
709
|
-
},
|
|
710
|
-
},
|
|
653
|
+
2: { 2: { 1: 0, 3: 0 } },
|
|
711
654
|
},
|
|
712
655
|
});
|
|
713
656
|
try {
|
|
714
|
-
const rsp = await this.sendOidbSvcTrpcTcp(
|
|
657
|
+
const rsp = await this.sendOidbSvcTrpcTcp(`OidbSvcTrpcTcp.0x11c${c2c ? 5 : 4}_200`, body);
|
|
715
658
|
const rkey = rsp[3][1];
|
|
716
659
|
if (!rkey)
|
|
717
660
|
throw new Error("rkey不存在");
|
|
@@ -732,7 +675,7 @@ async function getNTPicRkey() {
|
|
|
732
675
|
let needRefresh = false;
|
|
733
676
|
for (const key of Object.keys(rkey_cache)) {
|
|
734
677
|
const entry = rkey_cache[key];
|
|
735
|
-
if (!entry.value || entry.expire
|
|
678
|
+
if (!entry.value || (entry.expire || 0) - now < 600) { // 提前 10 分钟刷新
|
|
736
679
|
needRefresh = true;
|
|
737
680
|
break;
|
|
738
681
|
}
|
|
@@ -740,6 +683,21 @@ async function getNTPicRkey() {
|
|
|
740
683
|
if (!needRefresh) {
|
|
741
684
|
return rkey_cache;
|
|
742
685
|
}
|
|
686
|
+
// 如果已有请求正在进行中,直接复用其结果
|
|
687
|
+
if (rkey_fetching) {
|
|
688
|
+
return rkey_fetching;
|
|
689
|
+
}
|
|
690
|
+
rkey_fetching = _fetchNTPicRkey.call(this).finally(() => {
|
|
691
|
+
rkey_fetching = null;
|
|
692
|
+
});
|
|
693
|
+
return rkey_fetching;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* 实际执行获取QQNT图片rkey的请求
|
|
698
|
+
* @this {import("../ref").Client}
|
|
699
|
+
*/
|
|
700
|
+
async function _fetchNTPicRkey() {
|
|
743
701
|
const body = pb.encode({
|
|
744
702
|
1: {
|
|
745
703
|
1: {
|
|
@@ -776,36 +734,6 @@ async function getNTPicRkey() {
|
|
|
776
734
|
return rkey_cache;
|
|
777
735
|
}
|
|
778
736
|
|
|
779
|
-
/**
|
|
780
|
-
* 获取QQNT群图rkey
|
|
781
|
-
* @deprecated
|
|
782
|
-
* @this {import("../ref").Client}
|
|
783
|
-
*/
|
|
784
|
-
async function getGroupNTPicRkey() {
|
|
785
|
-
const rsp = await getGroupNTPicURL.call(this, {
|
|
786
|
-
2: "CNL-BhjClnU",
|
|
787
|
-
3: 1,
|
|
788
|
-
});
|
|
789
|
-
if (!rsp)
|
|
790
|
-
return "&rkey=CAQSKDOc_jvbthUjZrMxJG2jNZ-rIWue47Q3PGKb_GWljzyvOUZOv0-EVao";
|
|
791
|
-
return '&rkey=' + rsp.match(/&rkey=([^&]+)/)[1];
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
/**
|
|
795
|
-
* 获取QQNT私聊图rkey
|
|
796
|
-
* @deprecated
|
|
797
|
-
* @this {import("../ref").Client}
|
|
798
|
-
*/
|
|
799
|
-
async function getOffNTPicRkey() {
|
|
800
|
-
const rsp = await getOffNTPicURL.call(this, {
|
|
801
|
-
2: "CNL-BhjClnU",
|
|
802
|
-
3: 1,
|
|
803
|
-
});
|
|
804
|
-
if (!rsp)
|
|
805
|
-
return "&rkey=CAQSKDOc_jvbthUjAatuFPQIo-x9wwcDhDGd8SOEu5FyJWNxNMabJTTRpO8";
|
|
806
|
-
return '&rkey=' + rsp.match(/&rkey=([^&]+)/)[1];
|
|
807
|
-
}
|
|
808
|
-
|
|
809
737
|
/**
|
|
810
738
|
* 通过fileid获取QQNT图片URL
|
|
811
739
|
* @this {import("../ref").Client}
|
|
@@ -839,7 +767,7 @@ async function getNTPicURLbyFileidApi(fileid) {
|
|
|
839
767
|
module.exports = {
|
|
840
768
|
ImageBuilder, uploadImages, buildImageFileParam,
|
|
841
769
|
groupNTPicDown, offNTPicDown, checkImgUrl,
|
|
842
|
-
|
|
770
|
+
getNTPicURL,
|
|
843
771
|
getNTPicURLbyFileid,
|
|
844
772
|
getNTPicURLbyFileidApi,
|
|
845
773
|
};
|
package/lib/message/parser.js
CHANGED
|
@@ -10,7 +10,7 @@ const pb = require("../algo/pb");
|
|
|
10
10
|
const face = require("./face");
|
|
11
11
|
const { getGroupMsgs } = require("./history");
|
|
12
12
|
const { int32ip2str } = require("../service");
|
|
13
|
-
const { buildImageFileParam, groupNTPicDown, offNTPicDown, checkImgUrl,
|
|
13
|
+
const { buildImageFileParam, groupNTPicDown, offNTPicDown, checkImgUrl, getNTPicURL, getNTPicURLbyFileid } = require("./image");
|
|
14
14
|
const { Gfs, getC2CFileUrl, getSyncDeviceFileUrl } = require("./file");
|
|
15
15
|
const { getVideoUrl, getPttUrl, getOffNTPttUrl, getGroupNTPttUrl, getOffNTVideoUrl, getGroupNTVideoUrl } = require("./ptt");
|
|
16
16
|
const { genC2CMessageId, genGroupMessageId, timestamp, parseFunString, code2uin, genRandom, unzip } = require("../common");
|
|
@@ -589,9 +589,9 @@ class Parser {
|
|
|
589
589
|
// 图片地址失效,尝试发包获取
|
|
590
590
|
let newURL = null;
|
|
591
591
|
if (dm) { //私聊
|
|
592
|
-
newURL = await
|
|
592
|
+
newURL = await getNTPicURL.call(this.c, imgInfo, String(this.uid), true);
|
|
593
593
|
} else { //群聊
|
|
594
|
-
newURL = await
|
|
594
|
+
newURL = await getNTPicURL.call(this.c, imgInfo, this.gid);
|
|
595
595
|
}
|
|
596
596
|
if (newURL) {
|
|
597
597
|
data.url = newURL;
|
package/lib/oicq.js
CHANGED
|
@@ -216,8 +216,15 @@ function kickoffListener(data) {
|
|
|
216
216
|
|
|
217
217
|
Client.prototype._register = async function () {
|
|
218
218
|
if (!await this._wt.register()) {
|
|
219
|
+
if (++this._registerRetryCount > 10) {
|
|
220
|
+
this.logger.error("register重试超过10次,停止重试");
|
|
221
|
+
this._registerRetryCount = 0;
|
|
222
|
+
this.terminate();
|
|
223
|
+
return this.em("system.login.error", { code: -1, message: "上线注册失败,请检查token与协议类型是否匹配" });
|
|
224
|
+
}
|
|
219
225
|
return this.emit("internal.network", "服务器繁忙(register)");
|
|
220
226
|
}
|
|
227
|
+
this._registerRetryCount = 0;
|
|
221
228
|
this.status = STATUS_ONLINE;
|
|
222
229
|
if (!this.online_status)
|
|
223
230
|
this.online_status = 11;
|
|
@@ -292,6 +299,7 @@ Client.prototype._stopHeartbeat = function () {
|
|
|
292
299
|
*/
|
|
293
300
|
async function onlineListener() {
|
|
294
301
|
this.sync_finished = false;
|
|
302
|
+
this._registerRetryCount = 0;
|
|
295
303
|
await this._register();
|
|
296
304
|
if (!this.isOnline())
|
|
297
305
|
return;
|
package/lib/qsign-api.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QSign API 公共模块
|
|
3
|
+
* 封装所有与 qsign 服务器交互的通用逻辑
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const _axios = require("axios");
|
|
8
|
+
const http = require("http");
|
|
9
|
+
const https = require("https");
|
|
10
|
+
|
|
11
|
+
const API_WAIT_TIME = 45000;
|
|
12
|
+
const MAX_RETRY = 3;
|
|
13
|
+
|
|
14
|
+
const axios = _axios.create({
|
|
15
|
+
httpAgent: new http.Agent({ keepAlive: true }),
|
|
16
|
+
httpsAgent: new https.Agent({ keepAlive: true }),
|
|
17
|
+
timeout: API_WAIT_TIME,
|
|
18
|
+
headers: {
|
|
19
|
+
'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.199 Safari/537.36 OICQ/1.26 ILPP/2",
|
|
20
|
+
'Content-Type': "application/x-www-form-urlencoded"
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 构建 qsign API URL
|
|
26
|
+
* @param {string} baseAddr 基础地址
|
|
27
|
+
* @param {string} endpoint 端点路径
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function buildUrl(baseAddr, endpoint) {
|
|
31
|
+
let url = baseAddr;
|
|
32
|
+
if (url[url.length - 1] !== '/') {
|
|
33
|
+
url += '/';
|
|
34
|
+
}
|
|
35
|
+
return url + endpoint;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 获取 qsign API 公共参数
|
|
40
|
+
* @this {import("./ref").Client}
|
|
41
|
+
* @param {object} [extraParams] 额外参数
|
|
42
|
+
* @returns {object}
|
|
43
|
+
*/
|
|
44
|
+
function getCommonParams(extraParams = {}) {
|
|
45
|
+
return {
|
|
46
|
+
ver: this.apk.ver,
|
|
47
|
+
android_id: this.device.android_id,
|
|
48
|
+
androidId: this.device.android_id,
|
|
49
|
+
qimei36: this.device.qimei36 || this.device.qimei16 || this.device.android_id,
|
|
50
|
+
guid: this.device.guid.toString('hex'),
|
|
51
|
+
uin: this.uin,
|
|
52
|
+
key: this.config.sign_api_key,
|
|
53
|
+
qua: this.apk.qua,
|
|
54
|
+
...extraParams,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 发送 qsign GET 请求
|
|
60
|
+
* @param {string} url
|
|
61
|
+
* @param {object} params
|
|
62
|
+
* @returns {Promise<object>}
|
|
63
|
+
*/
|
|
64
|
+
async function qsignGet(url, params) {
|
|
65
|
+
const { data } = await axios.get(url, { params })
|
|
66
|
+
.catch((e) => ({ data: { code: -1, msg: e ? e.message : 'failed to connect to qsign api' } }));
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 发送 qsign POST 请求
|
|
72
|
+
* @param {string} url
|
|
73
|
+
* @param {object} body
|
|
74
|
+
* @returns {Promise<object>}
|
|
75
|
+
*/
|
|
76
|
+
async function qsignPost(url, body) {
|
|
77
|
+
const { data } = await axios.post(url, body)
|
|
78
|
+
.catch((e) => ({ data: { code: -1, msg: e ? e.message : 'failed to connect to qsign api' } }));
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 执行带重试的 qsign API 调用
|
|
84
|
+
* @param {function} apiFn 实际 API 调用函数
|
|
85
|
+
* @param {string} apiName API 名称(用于日志)
|
|
86
|
+
* @param {object} options 选项
|
|
87
|
+
* @param {boolean} [options.throwOnError=false] 是否在失败时抛出错误
|
|
88
|
+
* @param {boolean} [options.handleNotRegistered=false] 是否处理 "not registered" 错误
|
|
89
|
+
* @param {function} [options.onNotRegistered] 处理 "not registered" 的回调
|
|
90
|
+
* @param {number} [options.retry=0] 当前重试次数
|
|
91
|
+
* @returns {Promise<object>}
|
|
92
|
+
*/
|
|
93
|
+
async function executeWithRetry(apiFn, apiName, options = {}) {
|
|
94
|
+
const {
|
|
95
|
+
throwOnError = false,
|
|
96
|
+
handleNotRegistered = false,
|
|
97
|
+
onNotRegistered = null,
|
|
98
|
+
retry = 0,
|
|
99
|
+
logger = console,
|
|
100
|
+
} = options;
|
|
101
|
+
|
|
102
|
+
const data = await apiFn();
|
|
103
|
+
|
|
104
|
+
if (data.code !== 0) {
|
|
105
|
+
// 处理 "not registered" 错误
|
|
106
|
+
if (handleNotRegistered && data.code === 1 && String(data.msg).includes('not registered')) {
|
|
107
|
+
if (retry >= MAX_RETRY) {
|
|
108
|
+
data.msg = 'not registered after max retries. Original message: ' + data.msg;
|
|
109
|
+
const errorMsg = `[qsign][${apiName}] ${data.msg}(${data.code})`;
|
|
110
|
+
if (throwOnError) {
|
|
111
|
+
throw new Error(errorMsg);
|
|
112
|
+
}
|
|
113
|
+
logger.warn(errorMsg);
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
if (onNotRegistered) {
|
|
117
|
+
await onNotRegistered();
|
|
118
|
+
}
|
|
119
|
+
await sleep(100);
|
|
120
|
+
return executeWithRetry(apiFn, apiName, { ...options, retry: retry + 1 });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 重试逻辑
|
|
124
|
+
if (retry < MAX_RETRY) {
|
|
125
|
+
logger.warn(`[qsign][${apiName}] ${data.msg}(${data.code}), retry in 1s...`);
|
|
126
|
+
await sleep(1000);
|
|
127
|
+
return executeWithRetry(apiFn, apiName, { ...options, retry: retry + 1 });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 最终失败处理
|
|
131
|
+
data.msg = data.msg || 'unknown error';
|
|
132
|
+
const errorMsg = `[qsign][${apiName}] ${data.msg}(${data.code})`;
|
|
133
|
+
if (throwOnError) {
|
|
134
|
+
throw new Error(errorMsg);
|
|
135
|
+
}
|
|
136
|
+
logger.warn(errorMsg);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return data;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 延迟函数
|
|
144
|
+
* @param {number} time 毫秒
|
|
145
|
+
* @returns {Promise<void>}
|
|
146
|
+
*/
|
|
147
|
+
function sleep(time) {
|
|
148
|
+
return new Promise(resolve => setTimeout(resolve, time));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
buildUrl,
|
|
153
|
+
getCommonParams,
|
|
154
|
+
qsignGet,
|
|
155
|
+
qsignPost,
|
|
156
|
+
executeWithRetry,
|
|
157
|
+
sleep,
|
|
158
|
+
};
|
package/lib/ref.d.ts
CHANGED
|
@@ -195,6 +195,8 @@ export class Client extends oicq.Client {
|
|
|
195
195
|
submitSsoCallback(cmd: string, callbackId: number, body: Buffer): Promise<void>;
|
|
196
196
|
refreshQSignToken(): Promise<void>;
|
|
197
197
|
buildSSOReserveField(cmd: string, body: Buffer, seq?: number, options?: SsoOptions): Promise<Buffer>;
|
|
198
|
+
getXwDebugId(): Promise<void>;
|
|
199
|
+
updateCmdWhiteList(): Promise<void>;
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
export * from '../index';
|
|
@@ -52,6 +52,12 @@ WtLogin.prototype.passwordLogin = async function () {
|
|
|
52
52
|
} catch {
|
|
53
53
|
this.uid = "";
|
|
54
54
|
}
|
|
55
|
+
try {
|
|
56
|
+
await this.c.updateCmdWhiteList();
|
|
57
|
+
} catch (e) {
|
|
58
|
+
const message = e.message || "[qsign][CmdWhiteList] 未知错误";
|
|
59
|
+
this.logger.error(message);
|
|
60
|
+
}
|
|
55
61
|
try {
|
|
56
62
|
this.t106 = await fs.promises.readFile(path.join(this.dir, "t106"));
|
|
57
63
|
try {
|
package/lib/wtlogin/tlv.js
CHANGED
|
@@ -367,7 +367,7 @@ const tlv_map = {
|
|
|
367
367
|
const salt = new Writer()
|
|
368
368
|
const subCmd = parseInt('0x' + (code.split("_")[1] || 9))
|
|
369
369
|
if (["810_9", "810_a", "810_d", "810_f"].includes(code)) {
|
|
370
|
-
salt.writeU32(0)
|
|
370
|
+
salt.writeU32(this.uin > 2147483647 ? 0xffffffff : 0)
|
|
371
371
|
salt.writeTlv(this.device.guid)
|
|
372
372
|
salt.writeTlv(Buffer.from(this.apk.sdkver))
|
|
373
373
|
salt.writeU32(subCmd)
|
package/lib/wtlogin/wt.js
CHANGED
|
@@ -206,7 +206,6 @@ class WtLogin {
|
|
|
206
206
|
this.logger.warn(`Unsupported tlv546 hash type ${hashType}`);
|
|
207
207
|
return Buffer.alloc(0);
|
|
208
208
|
}
|
|
209
|
-
let inputNum = BigInt("0x" + src.toString("hex"));
|
|
210
209
|
switch (typ) {
|
|
211
210
|
case 1:
|
|
212
211
|
// TODO
|
|
@@ -219,10 +218,15 @@ class WtLogin {
|
|
|
219
218
|
let elp = 0, cnt = 0;
|
|
220
219
|
if (tgt.length === 32) {
|
|
221
220
|
const start = Date.now();
|
|
222
|
-
|
|
221
|
+
const buf = Buffer.alloc(128);
|
|
222
|
+
src.copy(buf, 128 - src.length);
|
|
223
|
+
let hash = createHash("sha256").update(buf).digest();
|
|
223
224
|
while (Buffer.compare(hash, tgt) !== 0) {
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
for (let i = 127; i >= 0; i--) {
|
|
226
|
+
buf[i] = (buf[i] + 1) & 0xFF;
|
|
227
|
+
if (buf[i] !== 0) break;
|
|
228
|
+
}
|
|
229
|
+
hash = createHash("sha256").update(buf).digest();
|
|
226
230
|
cnt++;
|
|
227
231
|
if (cnt > 1000000) {
|
|
228
232
|
this.logger.error("Calculating PoW cost too much time, maybe something wrong");
|
|
@@ -230,7 +234,7 @@ class WtLogin {
|
|
|
230
234
|
}
|
|
231
235
|
}
|
|
232
236
|
ok = true;
|
|
233
|
-
dst =
|
|
237
|
+
dst = buf;
|
|
234
238
|
elp = Date.now() - start;
|
|
235
239
|
this.logger.info(`Calculating PoW of plus ${cnt} times cost ${elp} ms`);
|
|
236
240
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oicq-icalingua-plus-plus",
|
|
3
3
|
"app_name": "oicq-icalingua-plus-plus",
|
|
4
|
-
"version": "1.26.
|
|
5
|
-
"app_version": "1.26.
|
|
6
|
-
"upday": "2026/
|
|
4
|
+
"version": "1.26.3",
|
|
5
|
+
"app_version": "1.26.3",
|
|
6
|
+
"upday": "2026/06/28",
|
|
7
7
|
"protocol_version": "v11",
|
|
8
8
|
"description": "oicq(https://github.com/takayama-lily/oicq) v1 for Icalingua++",
|
|
9
9
|
"main": "index.js",
|
|
@@ -21,23 +21,23 @@
|
|
|
21
21
|
"author": "The Icalingua++ Authors",
|
|
22
22
|
"license": "MPL-2.0",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"axios": "
|
|
25
|
-
"jsqr": "
|
|
26
|
-
"log4js": "
|
|
27
|
-
"long": "
|
|
28
|
-
"pngjs": "
|
|
29
|
-
"probe-image-size": "
|
|
30
|
-
"qrcode-terminal": "
|
|
31
|
-
"ws": "
|
|
24
|
+
"axios": "1.14.0",
|
|
25
|
+
"jsqr": "1.4.0",
|
|
26
|
+
"log4js": "6.9.1",
|
|
27
|
+
"long": "4.0.0",
|
|
28
|
+
"pngjs": "6.0.0",
|
|
29
|
+
"probe-image-size": "7.2.3",
|
|
30
|
+
"qrcode-terminal": "0.12.0",
|
|
31
|
+
"ws": "7.5.11"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^16.7.13",
|
|
35
|
-
"@types/probe-image-size": "
|
|
36
|
-
"@types/ws": "
|
|
37
|
-
"chai": "
|
|
38
|
-
"mocha": "
|
|
39
|
-
"mockjs": "
|
|
40
|
-
"protobufjs": "
|
|
35
|
+
"@types/probe-image-size": "7.0.1",
|
|
36
|
+
"@types/ws": "7.4.7",
|
|
37
|
+
"chai": "4.5.0",
|
|
38
|
+
"mocha": "8.4.0",
|
|
39
|
+
"mockjs": "1.1.0",
|
|
40
|
+
"protobufjs": "6.11.6"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"/bin",
|