wx2tg-puppet 3.0.8 → 6.0.16
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 +86 -0
- package/VERSION +1 -0
- package/dist/api/FriendApi.js +1 -1
- package/dist/api/ToolsApi.d.ts +7 -1
- package/dist/api/ToolsApi.js +26 -1
- package/dist/core/Contact.d.ts +5 -0
- package/dist/core/Contact.js +71 -30
- package/dist/core/Message.d.ts +2 -0
- package/dist/core/Message.js +43 -33
- package/dist/entities/ContactEntity.d.ts +1 -0
- package/dist/entities/ContactEntity.js +4 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +208 -158
- package/dist/services/contact.service.js +9 -13
- package/dist/types/MessageType.d.ts +1 -0
- package/dist/types/MessageType.js +1 -0
- package/dist/utils/CallbackPayload.d.ts +4 -0
- package/dist/utils/CallbackPayload.js +52 -0
- package/dist/utils/ContactClassification.d.ts +4 -0
- package/dist/utils/ContactClassification.js +21 -0
- package/dist/utils/FileDownloadError.d.ts +7 -0
- package/dist/utils/FileDownloadError.js +14 -0
- package/dist/utils/SolitairePayload.d.ts +5 -0
- package/dist/utils/SolitairePayload.js +78 -0
- package/dist/utils/downloadFileSection.d.ts +11 -0
- package/dist/utils/downloadFileSection.js +153 -0
- package/dist/utils/fileDownload.d.ts +9 -0
- package/dist/utils/fileDownload.js +57 -0
- package/package.json +50 -42
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
42
42
|
});
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.UrlLink = exports.WeVideo = exports.Voice = exports.Emoji = exports.Message = exports.Room = exports.Contact = exports.WxBot = exports.AppDataSource = void 0;
|
|
45
|
+
exports.UrlLink = exports.WeVideo = exports.Voice = exports.Emoji = exports.isOfficialAccountVerifyFlag = exports.normalizeContactVerifyFlag = exports.Message = exports.Room = exports.Contact = exports.WxBot = exports.AppDataSource = void 0;
|
|
46
46
|
require("reflect-metadata");
|
|
47
47
|
const axiosInstance_1 = require("./utils/axiosInstance");
|
|
48
48
|
const data_source_1 = require("./database/data-source");
|
|
@@ -73,6 +73,7 @@ const ContactEntity_1 = require("./entities/ContactEntity");
|
|
|
73
73
|
const room_service_1 = require("./services/room.service");
|
|
74
74
|
const message_service_1 = require("./services/message.service");
|
|
75
75
|
const http = __importStar(require("http"));
|
|
76
|
+
const CallbackPayload_1 = require("./utils/CallbackPayload");
|
|
76
77
|
class WxBot extends events_1.EventEmitter {
|
|
77
78
|
constructor({ baseUrl, databasePath, deviceType = 'ipad', mode = 'polling', callbackPort = 8056, ProxyIp, ProxyPassword, ProxyUser }) {
|
|
78
79
|
super();
|
|
@@ -81,13 +82,15 @@ class WxBot extends events_1.EventEmitter {
|
|
|
81
82
|
this.heartbeatInterval = null;
|
|
82
83
|
this.callbackInterval = null;
|
|
83
84
|
this.msgInterval = null;
|
|
84
|
-
this.recentMsgIds =
|
|
85
|
+
this.recentMsgIds = new Map();
|
|
86
|
+
this.messageQueue = Promise.resolve();
|
|
87
|
+
this.syncTask = null;
|
|
88
|
+
this.syncRequested = false;
|
|
89
|
+
this.loginRecoveryInProgress = false;
|
|
85
90
|
this.syncFailureCount = 0; // Sync failure counter
|
|
86
91
|
this.httpServer = null; // HTTP回调服务器
|
|
87
92
|
this.loggingOut = false; // Flag to prevent concurrent logOut calls
|
|
88
93
|
this.loggingIn = false; // Flag to prevent concurrent login calls
|
|
89
|
-
this.syncInProgress = false; // Prevent overlapping message sync calls
|
|
90
|
-
this.loginRecoveryInProgress = false; // Allow only one login recovery flow
|
|
91
94
|
this.baseUrl = baseUrl;
|
|
92
95
|
this.databasePath = databasePath;
|
|
93
96
|
this.deviceType = deviceType;
|
|
@@ -383,95 +386,103 @@ class WxBot extends events_1.EventEmitter {
|
|
|
383
386
|
DeviceType: this.deviceType,
|
|
384
387
|
});
|
|
385
388
|
}
|
|
386
|
-
//
|
|
387
|
-
this.setupHeartbeat(userName);
|
|
388
|
-
// 根据模式选择消息监听方式
|
|
389
|
+
// 先监听再启动心跳;不再通过 Sync 清空并丢弃登录期间的消息。
|
|
389
390
|
if (this.mode === 'callback') {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const resp = yield MsgApi_1.msgApi.Sync({
|
|
393
|
-
"Wxid": userName,
|
|
394
|
-
"Scene": 0,
|
|
395
|
-
"Synckey": ""
|
|
396
|
-
});
|
|
397
|
-
if (!(yield this.checkSyncLoginStatus(resp.data, userName))) {
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (resp.data.Success) {
|
|
401
|
-
if (resp.data.Message === '当前未有新消息') {
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
391
|
+
yield this.startCallbackServer(userName);
|
|
392
|
+
this.emit('login', userName);
|
|
406
393
|
this.callbackInterval = setInterval(() => {
|
|
407
|
-
this.
|
|
394
|
+
void this.handleMsg(userName).catch(() => console.error('回调兜底同步失败'));
|
|
408
395
|
}, 1000 * 60 * 5);
|
|
409
|
-
this.startCallbackServer(userName);
|
|
410
396
|
}
|
|
411
397
|
else {
|
|
412
398
|
this.startPollingMode(userName);
|
|
413
399
|
}
|
|
400
|
+
yield this.setupHeartbeat(userName);
|
|
414
401
|
});
|
|
415
402
|
}
|
|
416
403
|
startCallbackServer(userName) {
|
|
417
|
-
|
|
418
|
-
this.httpServer
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
404
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
405
|
+
if (this.httpServer) {
|
|
406
|
+
const previous = this.httpServer;
|
|
407
|
+
this.httpServer = null;
|
|
408
|
+
yield new Promise(resolve => previous.close(() => resolve()));
|
|
409
|
+
}
|
|
410
|
+
const server = http.createServer((req, res) => {
|
|
411
|
+
const reply = (status, data) => {
|
|
412
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
413
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
414
|
+
res.end(JSON.stringify(data));
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
if (req.method !== 'POST' || req.url !== `/message/callback/${userName}`) {
|
|
418
|
+
reply(404, { error: 'Not Found' });
|
|
419
|
+
req.resume();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
let size = 0;
|
|
423
|
+
let oversized = false;
|
|
435
424
|
let chunks = [];
|
|
436
425
|
req.on('data', (chunk) => {
|
|
437
|
-
|
|
438
|
-
|
|
426
|
+
size += chunk.length;
|
|
427
|
+
if (size > CallbackPayload_1.MAX_CALLBACK_BYTES) {
|
|
428
|
+
oversized = true;
|
|
429
|
+
chunks = [];
|
|
430
|
+
reply(413, { error: 'Callback body too large' });
|
|
431
|
+
}
|
|
432
|
+
else if (!oversized) {
|
|
433
|
+
chunks.push(chunk);
|
|
434
|
+
}
|
|
439
435
|
});
|
|
440
|
-
req.on('end', () => {
|
|
436
|
+
req.on('end', () => __awaiter(this, void 0, void 0, function* () {
|
|
437
|
+
if (oversized || res.destroyed)
|
|
438
|
+
return;
|
|
439
|
+
let data;
|
|
441
440
|
try {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
441
|
+
// Decode once, after joining chunks, so split UTF-8 characters are preserved.
|
|
442
|
+
data = (0, CallbackPayload_1.parseCallbackPayload)(Buffer.concat(chunks).toString('utf8'));
|
|
443
|
+
chunks = [];
|
|
445
444
|
}
|
|
446
|
-
catch (
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
450
|
-
res.end(JSON.stringify({ error: 'Invalid JSON', body: body }));
|
|
445
|
+
catch (_a) {
|
|
446
|
+
reply(400, { error: 'Invalid callback payload' });
|
|
447
|
+
return;
|
|
451
448
|
}
|
|
449
|
+
try {
|
|
450
|
+
yield this.handleCallbackMessage(userName, data);
|
|
451
|
+
reply(200, { success: true });
|
|
452
|
+
}
|
|
453
|
+
catch (_b) {
|
|
454
|
+
console.error('回调消息处理失败');
|
|
455
|
+
reply(500, { error: 'Callback processing failed' });
|
|
456
|
+
}
|
|
457
|
+
}));
|
|
458
|
+
req.on('error', () => reply(400, { error: 'Request error' }));
|
|
459
|
+
});
|
|
460
|
+
this.httpServer = server;
|
|
461
|
+
server.requestTimeout = 30000;
|
|
462
|
+
yield new Promise((resolve, reject) => {
|
|
463
|
+
const failed = (error) => {
|
|
464
|
+
if (this.httpServer === server)
|
|
465
|
+
this.httpServer = null;
|
|
466
|
+
reject(error);
|
|
467
|
+
};
|
|
468
|
+
server.once('error', failed);
|
|
469
|
+
server.listen(this.callbackPort, '0.0.0.0', () => {
|
|
470
|
+
server.removeListener('error', failed);
|
|
471
|
+
server.on('error', () => console.error('回调服务器错误'));
|
|
472
|
+
resolve();
|
|
452
473
|
});
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
handleCallbackMessage(userName_1) {
|
|
478
|
+
return __awaiter(this, arguments, void 0, function* (userName, data = null) {
|
|
479
|
+
if (data === null) {
|
|
480
|
+
yield this.handleMsg(userName);
|
|
458
481
|
}
|
|
459
482
|
else {
|
|
460
|
-
|
|
461
|
-
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
483
|
+
yield this.enqueueMessages(userName, data);
|
|
462
484
|
}
|
|
463
485
|
});
|
|
464
|
-
this.httpServer.listen(this.callbackPort, '0.0.0.0', () => {
|
|
465
|
-
console.log(`回调服务器启动成功,监听地址: http://0.0.0.0:${this.callbackPort}${`/message/callback/${userName}`}`);
|
|
466
|
-
});
|
|
467
|
-
this.httpServer.on('error', (error) => {
|
|
468
|
-
console.error('回调服务器启动失败:', error);
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
handleCallbackMessage(userName) {
|
|
472
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
473
|
-
yield this.handleMsg(userName);
|
|
474
|
-
});
|
|
475
486
|
}
|
|
476
487
|
startPollingMode(userName) {
|
|
477
488
|
// 防止重复定时
|
|
@@ -480,9 +491,9 @@ class WxBot extends events_1.EventEmitter {
|
|
|
480
491
|
this.msgInterval = null;
|
|
481
492
|
}
|
|
482
493
|
// 监听消息,每秒掉一次
|
|
483
|
-
this.msgInterval = setInterval(() =>
|
|
484
|
-
|
|
485
|
-
}
|
|
494
|
+
this.msgInterval = setInterval(() => {
|
|
495
|
+
void this.handleMsg(userName).catch(() => console.error('消息轮询失败'));
|
|
496
|
+
}, 1000);
|
|
486
497
|
this.emit('login', userName);
|
|
487
498
|
}
|
|
488
499
|
checkLoginAfterDelay(userName) {
|
|
@@ -501,14 +512,17 @@ class WxBot extends events_1.EventEmitter {
|
|
|
501
512
|
if (resp.data.Message === '用户可能退出') {
|
|
502
513
|
return false;
|
|
503
514
|
}
|
|
515
|
+
// Recovery checks also consume Sync; preserve messages returned by that check.
|
|
516
|
+
(0, CallbackPayload_1.validateSyncResponse)(resp.data);
|
|
517
|
+
yield this.processMessages(userName, resp.data);
|
|
504
518
|
return true;
|
|
505
519
|
});
|
|
506
520
|
}
|
|
521
|
+
// Preserve the single-flight login recovery fix from published puppet 3.0.8.
|
|
507
522
|
recoverLogin(userName) {
|
|
508
523
|
return __awaiter(this, void 0, void 0, function* () {
|
|
509
|
-
if (this.loginRecoveryInProgress)
|
|
524
|
+
if (this.loginRecoveryInProgress)
|
|
510
525
|
return false;
|
|
511
|
-
}
|
|
512
526
|
this.loginRecoveryInProgress = true;
|
|
513
527
|
try {
|
|
514
528
|
yield LoginApi_1.loginApi.LoginTwiceAutoAuth(userName);
|
|
@@ -549,99 +563,130 @@ class WxBot extends events_1.EventEmitter {
|
|
|
549
563
|
}
|
|
550
564
|
handleMsg(userName) {
|
|
551
565
|
return __awaiter(this, void 0, void 0, function* () {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
this.
|
|
566
|
+
this.syncRequested = true;
|
|
567
|
+
if (this.syncTask)
|
|
568
|
+
return this.syncTask;
|
|
569
|
+
this.syncTask = (() => __awaiter(this, void 0, void 0, function* () {
|
|
570
|
+
do {
|
|
571
|
+
this.syncRequested = false;
|
|
572
|
+
const resp = yield MsgApi_1.msgApi.Sync({ Wxid: userName, Scene: 0, Synckey: '' });
|
|
573
|
+
yield this.enqueueMessages(userName, resp.data);
|
|
574
|
+
} while (this.syncRequested);
|
|
575
|
+
}))();
|
|
556
576
|
try {
|
|
557
|
-
yield this.
|
|
577
|
+
yield this.syncTask;
|
|
558
578
|
}
|
|
559
579
|
finally {
|
|
560
|
-
this.
|
|
580
|
+
this.syncTask = null;
|
|
561
581
|
}
|
|
562
582
|
});
|
|
563
583
|
}
|
|
564
|
-
|
|
584
|
+
enqueueMessages(userName, data) {
|
|
585
|
+
(0, CallbackPayload_1.validateSyncResponse)(data);
|
|
586
|
+
const task = this.messageQueue.then(() => this.processMessages(userName, data));
|
|
587
|
+
// Both polling and callbacks use one parser/queue. A failed batch must not poison later ones.
|
|
588
|
+
this.messageQueue = task.catch(() => { });
|
|
589
|
+
return task;
|
|
590
|
+
}
|
|
591
|
+
processMessages(userName, data) {
|
|
565
592
|
return __awaiter(this, void 0, void 0, function* () {
|
|
566
593
|
var _a;
|
|
567
|
-
|
|
568
|
-
"Wxid": userName,
|
|
569
|
-
"Scene": 0,
|
|
570
|
-
"Synckey": ""
|
|
571
|
-
});
|
|
572
|
-
const data = resp.data;
|
|
573
|
-
if (!(yield this.checkSyncLoginStatus(data, userName))) {
|
|
594
|
+
if (!(yield this.checkSyncLoginStatus(data, userName)))
|
|
574
595
|
return;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
// Add to recent messages and maintain only last 40
|
|
592
|
-
this.recentMsgIds.push(newMsgId);
|
|
593
|
-
if (this.recentMsgIds.length > 40) {
|
|
594
|
-
this.recentMsgIds.shift(); // Remove oldest message ID
|
|
595
|
-
}
|
|
596
|
-
// 将msg转换为Message类型
|
|
597
|
-
const message = new Message_1.Message({
|
|
598
|
-
MsgId: msg.MsgId,
|
|
599
|
-
FromUserName: msg.FromUserName.string.endsWith('@chatroom') ? msg.Content.string.split(':\n')[0] : msg.FromUserName.string,
|
|
600
|
-
ToUserName: msg.FromUserName.string.endsWith('@chatroom') ? msg.FromUserName.string : msg.ToUserName.string,
|
|
601
|
-
MsgType: msg.MsgType,
|
|
602
|
-
Content: msg.FromUserName.string.endsWith('@chatroom') ? msg.Content.string.split(':\n').slice(1).join(':\n') : msg.Content.string,
|
|
603
|
-
Status: msg.Status,
|
|
604
|
-
ImgStatus: msg.ImgStatus,
|
|
605
|
-
ImgBuf: msg.ImgBuf.iLen,
|
|
606
|
-
CreateTime: msg.CreateTime,
|
|
607
|
-
MsgSource: msg.MsgSource,
|
|
608
|
-
NewMsgId: msg.NewMsgId,
|
|
609
|
-
MsgSeq: msg.MsgSeq,
|
|
610
|
-
RoomId: (msg.FromUserName.string.endsWith('@chatroom') ? msg.FromUserName.string : '') || (msg.ToUserName.string.endsWith('@chatroom') ? msg.ToUserName.string : ''),
|
|
611
|
-
IsSelf: msg.FromUserName.string === userName,
|
|
612
|
-
xml: msg.Content.string
|
|
613
|
-
});
|
|
614
|
-
// 处理企业微信
|
|
615
|
-
if ((_a = message.fromId) === null || _a === void 0 ? void 0 : _a.includes('@openim')) {
|
|
616
|
-
let contactE = yield contact_service_1.contactService.getByUserName(message.fromId);
|
|
617
|
-
if (!contactE) {
|
|
618
|
-
contactE = new ContactEntity_1.ContactEntity();
|
|
596
|
+
if (!data.Success || !((_a = data.Data) === null || _a === void 0 ? void 0 : _a.AddMsgs))
|
|
597
|
+
return;
|
|
598
|
+
let failed = false;
|
|
599
|
+
for (const msg of data.Data.AddMsgs) {
|
|
600
|
+
try {
|
|
601
|
+
// One malformed entry must not discard unrelated messages in this consumed batch.
|
|
602
|
+
(0, CallbackPayload_1.validateRawMessage)(msg);
|
|
603
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
604
|
+
try {
|
|
605
|
+
yield this.processMessage(userName, msg);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
if (attempt === 2)
|
|
610
|
+
throw error;
|
|
611
|
+
yield new Promise(resolve => setTimeout(resolve, 100));
|
|
619
612
|
}
|
|
620
|
-
contactE.nickName = msg.PushContent.split(':')[0].slice(0, -1);
|
|
621
|
-
contactE.userName = message.fromId;
|
|
622
|
-
contactE.smallHeadImgUrl = 'https://raw.githubusercontent.com/finalpi/wechat2tg/wx2tg-pad/qywx.jpg';
|
|
623
|
-
contactE.bigHeadImgUrl = 'https://raw.githubusercontent.com/finalpi/wechat2tg/wx2tg-pad/qywx.jpg';
|
|
624
|
-
contact_service_1.contactService.upsertByUsername(contactE.userName, contactE);
|
|
625
|
-
}
|
|
626
|
-
if (message.type() === MessageType_1.MessageType.AddFriend) { // 好友请求
|
|
627
|
-
const obj = Message_1.Message.getXmlToJson(message.text());
|
|
628
|
-
console.log(obj);
|
|
629
|
-
const friendship = new Friendship_1.Friendship({
|
|
630
|
-
fromnickname: obj.msg.fromnickname,
|
|
631
|
-
v3: obj.msg.encryptusername,
|
|
632
|
-
v4: obj.msg.ticket,
|
|
633
|
-
scene: obj.msg.scene,
|
|
634
|
-
fromusername: obj.msg.fromusername,
|
|
635
|
-
bigHeadImgUrl: obj.msg.bigHeadImgUrl,
|
|
636
|
-
smallHeadImgUrl: obj.msg.smallHeadImgUrl,
|
|
637
|
-
content: obj.msg.content
|
|
638
|
-
});
|
|
639
|
-
this.emit('friendship', friendship);
|
|
640
|
-
}
|
|
641
|
-
else {
|
|
642
|
-
this.emit('message', message);
|
|
643
613
|
}
|
|
644
614
|
}
|
|
615
|
+
catch (_b) {
|
|
616
|
+
failed = true;
|
|
617
|
+
console.error('单条微信消息处理失败,其余消息继续处理');
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (failed)
|
|
621
|
+
throw new Error('Some messages could not be processed');
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
processMessage(userName, msg) {
|
|
625
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
626
|
+
var _a, _b;
|
|
627
|
+
const now = Date.now();
|
|
628
|
+
for (const [key, seenAt] of this.recentMsgIds) {
|
|
629
|
+
if (now - seenAt < 120000)
|
|
630
|
+
break;
|
|
631
|
+
this.recentMsgIds.delete(key);
|
|
632
|
+
}
|
|
633
|
+
const newMsgId = String(msg.NewMsgId);
|
|
634
|
+
const identity = newMsgId !== '0' ? newMsgId
|
|
635
|
+
: `${msg.MsgId}:${msg.FromUserName.string}:${msg.ToUserName.string}:${msg.CreateTime}`;
|
|
636
|
+
const key = `${userName}:${identity}`;
|
|
637
|
+
if (this.recentMsgIds.has(key))
|
|
638
|
+
return;
|
|
639
|
+
// 将msg转换为Message类型
|
|
640
|
+
const message = new Message_1.Message({
|
|
641
|
+
MsgId: msg.MsgId,
|
|
642
|
+
FromUserName: msg.FromUserName.string.endsWith('@chatroom') ? msg.Content.string.split(':\n')[0] : msg.FromUserName.string,
|
|
643
|
+
ToUserName: msg.FromUserName.string.endsWith('@chatroom') ? msg.FromUserName.string : msg.ToUserName.string,
|
|
644
|
+
MsgType: msg.MsgType,
|
|
645
|
+
Content: msg.FromUserName.string.endsWith('@chatroom') ? msg.Content.string.split(':\n').slice(1).join(':\n') : msg.Content.string,
|
|
646
|
+
Status: msg.Status,
|
|
647
|
+
ImgStatus: msg.ImgStatus,
|
|
648
|
+
ImgBuf: ((_a = msg.ImgBuf) === null || _a === void 0 ? void 0 : _a.iLen) || 0,
|
|
649
|
+
CreateTime: msg.CreateTime,
|
|
650
|
+
MsgSource: msg.MsgSource,
|
|
651
|
+
NewMsgId: newMsgId,
|
|
652
|
+
MsgSeq: msg.MsgSeq,
|
|
653
|
+
RoomId: (msg.FromUserName.string.endsWith('@chatroom') ? msg.FromUserName.string : '') || (msg.ToUserName.string.endsWith('@chatroom') ? msg.ToUserName.string : ''),
|
|
654
|
+
IsSelf: msg.FromUserName.string === userName,
|
|
655
|
+
xml: msg.Content.string
|
|
656
|
+
});
|
|
657
|
+
// 处理企业微信
|
|
658
|
+
if ((_b = message.fromId) === null || _b === void 0 ? void 0 : _b.includes('@openim')) {
|
|
659
|
+
let contactE = yield contact_service_1.contactService.getByUserName(message.fromId);
|
|
660
|
+
if (!contactE) {
|
|
661
|
+
contactE = new ContactEntity_1.ContactEntity();
|
|
662
|
+
}
|
|
663
|
+
contactE.nickName = msg.PushContent.split(':')[0].slice(0, -1);
|
|
664
|
+
contactE.userName = message.fromId;
|
|
665
|
+
contactE.smallHeadImgUrl = 'https://raw.githubusercontent.com/finalpi/wechat2tg/wx2tg-pad/qywx.jpg';
|
|
666
|
+
contactE.bigHeadImgUrl = 'https://raw.githubusercontent.com/finalpi/wechat2tg/wx2tg-pad/qywx.jpg';
|
|
667
|
+
yield contact_service_1.contactService.upsertByUsername(contactE.userName, contactE);
|
|
668
|
+
}
|
|
669
|
+
if (message.type() === MessageType_1.MessageType.AddFriend) { // 好友请求
|
|
670
|
+
const obj = Message_1.Message.getXmlToJson(message.text());
|
|
671
|
+
const friendship = new Friendship_1.Friendship({
|
|
672
|
+
fromnickname: obj.msg.fromnickname,
|
|
673
|
+
v3: obj.msg.encryptusername,
|
|
674
|
+
v4: obj.msg.ticket,
|
|
675
|
+
scene: obj.msg.scene,
|
|
676
|
+
fromusername: obj.msg.fromusername,
|
|
677
|
+
bigHeadImgUrl: obj.msg.bigHeadImgUrl,
|
|
678
|
+
smallHeadImgUrl: obj.msg.smallHeadImgUrl,
|
|
679
|
+
content: obj.msg.content
|
|
680
|
+
});
|
|
681
|
+
this.emit('friendship', friendship);
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
this.emit('message', message);
|
|
685
|
+
}
|
|
686
|
+
// Mark only after successful dispatch so a synchronous failure can be retried.
|
|
687
|
+
this.recentMsgIds.set(key, now);
|
|
688
|
+
if (this.recentMsgIds.size > 10000) {
|
|
689
|
+
this.recentMsgIds.delete(this.recentMsgIds.keys().next().value);
|
|
645
690
|
}
|
|
646
691
|
});
|
|
647
692
|
}
|
|
@@ -713,6 +758,7 @@ class WxBot extends events_1.EventEmitter {
|
|
|
713
758
|
});
|
|
714
759
|
if (resp.data.Code === 0) {
|
|
715
760
|
yield this.loginSuccess(config.wxid);
|
|
761
|
+
yield this.enqueueMessages(config.wxid, resp.data);
|
|
716
762
|
}
|
|
717
763
|
else {
|
|
718
764
|
yield this.login();
|
|
@@ -742,6 +788,7 @@ class WxBot extends events_1.EventEmitter {
|
|
|
742
788
|
yield LoginApi_1.loginApi.LoginTwiceAutoAuth(config === null || config === void 0 ? void 0 : config.wxid);
|
|
743
789
|
if (yield this.checkSyncLoginStatus(resp.data, config === null || config === void 0 ? void 0 : config.wxid)) {
|
|
744
790
|
yield this.loginSuccess(config.wxid);
|
|
791
|
+
yield this.enqueueMessages(config.wxid, resp.data);
|
|
745
792
|
return true;
|
|
746
793
|
}
|
|
747
794
|
else {
|
|
@@ -769,3 +816,6 @@ class WxBot extends events_1.EventEmitter {
|
|
|
769
816
|
}
|
|
770
817
|
}
|
|
771
818
|
exports.WxBot = WxBot;
|
|
819
|
+
var ContactClassification_1 = require("./utils/ContactClassification");
|
|
820
|
+
Object.defineProperty(exports, "normalizeContactVerifyFlag", { enumerable: true, get: function () { return ContactClassification_1.normalizeContactVerifyFlag; } });
|
|
821
|
+
Object.defineProperty(exports, "isOfficialAccountVerifyFlag", { enumerable: true, get: function () { return ContactClassification_1.isOfficialAccountVerifyFlag; } });
|
|
@@ -35,11 +35,11 @@ class ContactService {
|
|
|
35
35
|
}
|
|
36
36
|
update(userName, update) {
|
|
37
37
|
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return this.
|
|
38
|
+
// Update only supplied columns, not a stale snapshot of the whole row.
|
|
39
|
+
if (Object.values(update).some(value => value !== undefined)) {
|
|
40
|
+
yield this.repo.update({ userName }, update);
|
|
41
|
+
}
|
|
42
|
+
return this.getByUserName(userName);
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
clearRoom() {
|
|
@@ -56,14 +56,10 @@ class ContactService {
|
|
|
56
56
|
}
|
|
57
57
|
upsertByUsername(userName, data) {
|
|
58
58
|
return __awaiter(this, void 0, void 0, function* () {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
contact = this.repo.create(Object.assign({ userName }, data));
|
|
65
|
-
}
|
|
66
|
-
return this.repo.save(contact);
|
|
59
|
+
// INSERT ... ON CONFLICT avoids concurrent read-then-insert UNIQUE failures.
|
|
60
|
+
// TypeORM excludes undefined columns from the conflict update.
|
|
61
|
+
yield this.repo.upsert(Object.assign(Object.assign({}, data), { userName }), ['userName']);
|
|
62
|
+
return this.repo.findOneByOrFail({ userName });
|
|
67
63
|
});
|
|
68
64
|
}
|
|
69
65
|
delete(userName) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MAX_CALLBACK_BYTES = void 0;
|
|
7
|
+
exports.validateSyncResponse = validateSyncResponse;
|
|
8
|
+
exports.validateRawMessage = validateRawMessage;
|
|
9
|
+
exports.parseCallbackPayload = parseCallbackPayload;
|
|
10
|
+
const json_bigint_1 = __importDefault(require("json-bigint"));
|
|
11
|
+
const json = (0, json_bigint_1.default)({ storeAsString: true });
|
|
12
|
+
exports.MAX_CALLBACK_BYTES = 8 * 1024 * 1024;
|
|
13
|
+
// Go time.Time.String(), including its optional monotonic-clock suffix.
|
|
14
|
+
const timestampNotification = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? [+-]\d{4} \S+(?: m=[+-]\d+(?:\.\d+)?)?$/;
|
|
15
|
+
function validateSyncResponse(data) {
|
|
16
|
+
var _a;
|
|
17
|
+
if (!data || typeof data !== 'object' || !Number.isInteger(data.Code) || typeof data.Success !== 'boolean') {
|
|
18
|
+
throw new Error('Invalid sync envelope');
|
|
19
|
+
}
|
|
20
|
+
if (data.Code !== 0 || !data.Success)
|
|
21
|
+
return;
|
|
22
|
+
if (!data.Data || typeof data.Data !== 'object')
|
|
23
|
+
throw new Error('Invalid sync data');
|
|
24
|
+
const messages = data.Data.AddMsgs;
|
|
25
|
+
if (messages == null) {
|
|
26
|
+
// The v6 server retains AddMsgs. Do not silently acknowledge an unknown message format.
|
|
27
|
+
if ((_a = data.Data.Messages) === null || _a === void 0 ? void 0 : _a.length)
|
|
28
|
+
throw new Error('Missing raw messages');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!Array.isArray(messages))
|
|
32
|
+
throw new Error('Invalid message list');
|
|
33
|
+
}
|
|
34
|
+
function validateRawMessage(msg) {
|
|
35
|
+
var _a, _b, _c;
|
|
36
|
+
if (!msg || typeof ((_a = msg.FromUserName) === null || _a === void 0 ? void 0 : _a.string) !== 'string' || typeof ((_b = msg.ToUserName) === null || _b === void 0 ? void 0 : _b.string) !== 'string'
|
|
37
|
+
|| typeof ((_c = msg.Content) === null || _c === void 0 ? void 0 : _c.string) !== 'string' || !Number.isInteger(msg.MsgType)
|
|
38
|
+
|| !Number.isInteger(msg.CreateTime)
|
|
39
|
+
|| !((typeof msg.NewMsgId === 'string' && /^\d+$/.test(msg.NewMsgId))
|
|
40
|
+
|| (typeof msg.NewMsgId === 'number' && Number.isSafeInteger(msg.NewMsgId) && msg.NewMsgId >= 0))) {
|
|
41
|
+
throw new Error('Invalid raw message');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// null denotes a legacy wake-up notification, not a batch with an empty message list.
|
|
45
|
+
function parseCallbackPayload(body) {
|
|
46
|
+
const text = body.trim();
|
|
47
|
+
if (!text || timestampNotification.test(text))
|
|
48
|
+
return null;
|
|
49
|
+
const data = json.parse(text);
|
|
50
|
+
validateSyncResponse(data);
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Normalize an API VerifyFlag without turning missing/invalid data into personal contacts. */
|
|
2
|
+
export declare function normalizeContactVerifyFlag(value: unknown): number | null;
|
|
3
|
+
/** MM_USERATTRVERIFYFALG_BIZ_BRAND (8), not a username convention or any nonzero flag. */
|
|
4
|
+
export declare function isOfficialAccountVerifyFlag(value: unknown): boolean | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeContactVerifyFlag = normalizeContactVerifyFlag;
|
|
4
|
+
exports.isOfficialAccountVerifyFlag = isOfficialAccountVerifyFlag;
|
|
5
|
+
/** Normalize an API VerifyFlag without turning missing/invalid data into personal contacts. */
|
|
6
|
+
function normalizeContactVerifyFlag(value) {
|
|
7
|
+
if (typeof value === 'string') {
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
// uint32 numbers stringify as canonical decimal (no whitespace, signs or leading zeros).
|
|
10
|
+
if (String(parsed) !== value)
|
|
11
|
+
return null;
|
|
12
|
+
value = parsed;
|
|
13
|
+
}
|
|
14
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0xffffffff
|
|
15
|
+
? value : null;
|
|
16
|
+
}
|
|
17
|
+
/** MM_USERATTRVERIFYFALG_BIZ_BRAND (8), not a username convention or any nonzero flag. */
|
|
18
|
+
function isOfficialAccountVerifyFlag(value) {
|
|
19
|
+
const flag = normalizeContactVerifyFlag(value);
|
|
20
|
+
return flag === null ? undefined : (flag & 8) !== 0;
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Only static diagnostics and numeric status codes belong in download errors. */
|
|
2
|
+
export declare class FileDownloadError extends Error {
|
|
3
|
+
httpStatus?: number | undefined;
|
|
4
|
+
businessCode?: number | undefined;
|
|
5
|
+
retryable: boolean;
|
|
6
|
+
constructor(message: string, httpStatus?: number | undefined, businessCode?: number | undefined, retryable?: boolean);
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileDownloadError = void 0;
|
|
4
|
+
/** Only static diagnostics and numeric status codes belong in download errors. */
|
|
5
|
+
class FileDownloadError extends Error {
|
|
6
|
+
constructor(message, httpStatus, businessCode, retryable = false) {
|
|
7
|
+
super(`DownloadFile: ${message}`);
|
|
8
|
+
this.httpStatus = httpStatus;
|
|
9
|
+
this.businessCode = businessCode;
|
|
10
|
+
this.retryable = retryable;
|
|
11
|
+
this.name = 'FileDownloadError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.FileDownloadError = FileDownloadError;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const SOLITAIRE_FALLBACK = "[\u63A5\u9F99] \u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u89E3\u6790\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u67E5\u770B\u3002";
|
|
2
|
+
/** Bound solitaire display parsing; do not impose this size limit on other app types. */
|
|
3
|
+
export declare function isBoundedAppMessageXml(value: string): boolean;
|
|
4
|
+
/** Only appmsg.title is display text; solitaire_info is deliberately never interpreted. */
|
|
5
|
+
export declare function getSolitaireDisplayText(payload: string): string;
|