wx2tg-puppet 3.0.9 → 6.0.17
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 +81 -25
- package/VERSION +1 -0
- package/dist/api/FriendApi.js +1 -1
- package/dist/api/ToolsApi.d.ts +6 -4
- package/dist/api/ToolsApi.js +10 -8
- 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 +50 -16
- 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 +231 -164
- 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 +5 -16
- package/dist/utils/fileDownload.js +15 -134
- package/package.json +50 -43
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;
|
|
@@ -339,14 +342,31 @@ class WxBot extends events_1.EventEmitter {
|
|
|
339
342
|
// 处理当前批次的联系人
|
|
340
343
|
if (data.Data && data.Data.ContactUsernameList) {
|
|
341
344
|
console.log(`获取到 ${data.Data.ContactUsernameList.length} 个联系人`);
|
|
342
|
-
data.Data.ContactUsernameList
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
345
|
+
const contacts = data.Data.ContactUsernameList;
|
|
346
|
+
let nextIndex = 0;
|
|
347
|
+
let succeeded = 0;
|
|
348
|
+
let failed = 0;
|
|
349
|
+
// Bound detail requests and finish this page before requesting the next.
|
|
350
|
+
const worker = () => __awaiter(this, void 0, void 0, function* () {
|
|
351
|
+
while (nextIndex < contacts.length) {
|
|
352
|
+
const contact = contacts[nextIndex++];
|
|
353
|
+
try {
|
|
354
|
+
if (!contact.endsWith('@chatroom')) {
|
|
355
|
+
yield Contact_1.Contact.fetchAndUpsertContactDetail(userName, contact);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
yield Room_1.Room.find({ id: contact });
|
|
359
|
+
}
|
|
360
|
+
succeeded++;
|
|
361
|
+
}
|
|
362
|
+
catch (_a) {
|
|
363
|
+
// A missing/failed item must not abort the page or leak private data.
|
|
364
|
+
failed++;
|
|
365
|
+
}
|
|
348
366
|
}
|
|
349
367
|
});
|
|
368
|
+
yield Promise.all(Array.from({ length: Math.min(4, contacts.length) }, () => worker()));
|
|
369
|
+
console.log(`联系人批次处理完成: 成功 ${succeeded},失败 ${failed}`);
|
|
350
370
|
}
|
|
351
371
|
// 检查是否还有更多联系人需要获取
|
|
352
372
|
// CountinueFlag 为 1 表示还有更多数据
|
|
@@ -383,95 +403,103 @@ class WxBot extends events_1.EventEmitter {
|
|
|
383
403
|
DeviceType: this.deviceType,
|
|
384
404
|
});
|
|
385
405
|
}
|
|
386
|
-
//
|
|
387
|
-
this.setupHeartbeat(userName);
|
|
388
|
-
// 根据模式选择消息监听方式
|
|
406
|
+
// 先监听再启动心跳;不再通过 Sync 清空并丢弃登录期间的消息。
|
|
389
407
|
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
|
-
}
|
|
408
|
+
yield this.startCallbackServer(userName);
|
|
409
|
+
this.emit('login', userName);
|
|
406
410
|
this.callbackInterval = setInterval(() => {
|
|
407
|
-
this.
|
|
411
|
+
void this.handleMsg(userName).catch(() => console.error('回调兜底同步失败'));
|
|
408
412
|
}, 1000 * 60 * 5);
|
|
409
|
-
this.startCallbackServer(userName);
|
|
410
413
|
}
|
|
411
414
|
else {
|
|
412
415
|
this.startPollingMode(userName);
|
|
413
416
|
}
|
|
417
|
+
yield this.setupHeartbeat(userName);
|
|
414
418
|
});
|
|
415
419
|
}
|
|
416
420
|
startCallbackServer(userName) {
|
|
417
|
-
|
|
418
|
-
this.httpServer
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
421
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
422
|
+
if (this.httpServer) {
|
|
423
|
+
const previous = this.httpServer;
|
|
424
|
+
this.httpServer = null;
|
|
425
|
+
yield new Promise(resolve => previous.close(() => resolve()));
|
|
426
|
+
}
|
|
427
|
+
const server = http.createServer((req, res) => {
|
|
428
|
+
const reply = (status, data) => {
|
|
429
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
430
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
431
|
+
res.end(JSON.stringify(data));
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
if (req.method !== 'POST' || req.url !== `/message/callback/${userName}`) {
|
|
435
|
+
reply(404, { error: 'Not Found' });
|
|
436
|
+
req.resume();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
let size = 0;
|
|
440
|
+
let oversized = false;
|
|
435
441
|
let chunks = [];
|
|
436
442
|
req.on('data', (chunk) => {
|
|
437
|
-
|
|
438
|
-
|
|
443
|
+
size += chunk.length;
|
|
444
|
+
if (size > CallbackPayload_1.MAX_CALLBACK_BYTES) {
|
|
445
|
+
oversized = true;
|
|
446
|
+
chunks = [];
|
|
447
|
+
reply(413, { error: 'Callback body too large' });
|
|
448
|
+
}
|
|
449
|
+
else if (!oversized) {
|
|
450
|
+
chunks.push(chunk);
|
|
451
|
+
}
|
|
439
452
|
});
|
|
440
|
-
req.on('end', () => {
|
|
453
|
+
req.on('end', () => __awaiter(this, void 0, void 0, function* () {
|
|
454
|
+
if (oversized || res.destroyed)
|
|
455
|
+
return;
|
|
456
|
+
let data;
|
|
441
457
|
try {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
458
|
+
// Decode once, after joining chunks, so split UTF-8 characters are preserved.
|
|
459
|
+
data = (0, CallbackPayload_1.parseCallbackPayload)(Buffer.concat(chunks).toString('utf8'));
|
|
460
|
+
chunks = [];
|
|
445
461
|
}
|
|
446
|
-
catch (
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
450
|
-
res.end(JSON.stringify({ error: 'Invalid JSON', body: body }));
|
|
462
|
+
catch (_a) {
|
|
463
|
+
reply(400, { error: 'Invalid callback payload' });
|
|
464
|
+
return;
|
|
451
465
|
}
|
|
466
|
+
try {
|
|
467
|
+
yield this.handleCallbackMessage(userName, data);
|
|
468
|
+
reply(200, { success: true });
|
|
469
|
+
}
|
|
470
|
+
catch (_b) {
|
|
471
|
+
console.error('回调消息处理失败');
|
|
472
|
+
reply(500, { error: 'Callback processing failed' });
|
|
473
|
+
}
|
|
474
|
+
}));
|
|
475
|
+
req.on('error', () => reply(400, { error: 'Request error' }));
|
|
476
|
+
});
|
|
477
|
+
this.httpServer = server;
|
|
478
|
+
server.requestTimeout = 30000;
|
|
479
|
+
yield new Promise((resolve, reject) => {
|
|
480
|
+
const failed = (error) => {
|
|
481
|
+
if (this.httpServer === server)
|
|
482
|
+
this.httpServer = null;
|
|
483
|
+
reject(error);
|
|
484
|
+
};
|
|
485
|
+
server.once('error', failed);
|
|
486
|
+
server.listen(this.callbackPort, '0.0.0.0', () => {
|
|
487
|
+
server.removeListener('error', failed);
|
|
488
|
+
server.on('error', () => console.error('回调服务器错误'));
|
|
489
|
+
resolve();
|
|
452
490
|
});
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
handleCallbackMessage(userName_1) {
|
|
495
|
+
return __awaiter(this, arguments, void 0, function* (userName, data = null) {
|
|
496
|
+
if (data === null) {
|
|
497
|
+
yield this.handleMsg(userName);
|
|
458
498
|
}
|
|
459
499
|
else {
|
|
460
|
-
|
|
461
|
-
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
500
|
+
yield this.enqueueMessages(userName, data);
|
|
462
501
|
}
|
|
463
502
|
});
|
|
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
503
|
}
|
|
476
504
|
startPollingMode(userName) {
|
|
477
505
|
// 防止重复定时
|
|
@@ -480,9 +508,9 @@ class WxBot extends events_1.EventEmitter {
|
|
|
480
508
|
this.msgInterval = null;
|
|
481
509
|
}
|
|
482
510
|
// 监听消息,每秒掉一次
|
|
483
|
-
this.msgInterval = setInterval(() =>
|
|
484
|
-
|
|
485
|
-
}
|
|
511
|
+
this.msgInterval = setInterval(() => {
|
|
512
|
+
void this.handleMsg(userName).catch(() => console.error('消息轮询失败'));
|
|
513
|
+
}, 1000);
|
|
486
514
|
this.emit('login', userName);
|
|
487
515
|
}
|
|
488
516
|
checkLoginAfterDelay(userName) {
|
|
@@ -501,14 +529,17 @@ class WxBot extends events_1.EventEmitter {
|
|
|
501
529
|
if (resp.data.Message === '用户可能退出') {
|
|
502
530
|
return false;
|
|
503
531
|
}
|
|
532
|
+
// Recovery checks also consume Sync; preserve messages returned by that check.
|
|
533
|
+
(0, CallbackPayload_1.validateSyncResponse)(resp.data);
|
|
534
|
+
yield this.processMessages(userName, resp.data);
|
|
504
535
|
return true;
|
|
505
536
|
});
|
|
506
537
|
}
|
|
538
|
+
// Preserve the single-flight login recovery fix from published puppet 3.0.8.
|
|
507
539
|
recoverLogin(userName) {
|
|
508
540
|
return __awaiter(this, void 0, void 0, function* () {
|
|
509
|
-
if (this.loginRecoveryInProgress)
|
|
541
|
+
if (this.loginRecoveryInProgress)
|
|
510
542
|
return false;
|
|
511
|
-
}
|
|
512
543
|
this.loginRecoveryInProgress = true;
|
|
513
544
|
try {
|
|
514
545
|
yield LoginApi_1.loginApi.LoginTwiceAutoAuth(userName);
|
|
@@ -549,99 +580,130 @@ class WxBot extends events_1.EventEmitter {
|
|
|
549
580
|
}
|
|
550
581
|
handleMsg(userName) {
|
|
551
582
|
return __awaiter(this, void 0, void 0, function* () {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
this.
|
|
583
|
+
this.syncRequested = true;
|
|
584
|
+
if (this.syncTask)
|
|
585
|
+
return this.syncTask;
|
|
586
|
+
this.syncTask = (() => __awaiter(this, void 0, void 0, function* () {
|
|
587
|
+
do {
|
|
588
|
+
this.syncRequested = false;
|
|
589
|
+
const resp = yield MsgApi_1.msgApi.Sync({ Wxid: userName, Scene: 0, Synckey: '' });
|
|
590
|
+
yield this.enqueueMessages(userName, resp.data);
|
|
591
|
+
} while (this.syncRequested);
|
|
592
|
+
}))();
|
|
556
593
|
try {
|
|
557
|
-
yield this.
|
|
594
|
+
yield this.syncTask;
|
|
558
595
|
}
|
|
559
596
|
finally {
|
|
560
|
-
this.
|
|
597
|
+
this.syncTask = null;
|
|
561
598
|
}
|
|
562
599
|
});
|
|
563
600
|
}
|
|
564
|
-
|
|
601
|
+
enqueueMessages(userName, data) {
|
|
602
|
+
(0, CallbackPayload_1.validateSyncResponse)(data);
|
|
603
|
+
const task = this.messageQueue.then(() => this.processMessages(userName, data));
|
|
604
|
+
// Both polling and callbacks use one parser/queue. A failed batch must not poison later ones.
|
|
605
|
+
this.messageQueue = task.catch(() => { });
|
|
606
|
+
return task;
|
|
607
|
+
}
|
|
608
|
+
processMessages(userName, data) {
|
|
565
609
|
return __awaiter(this, void 0, void 0, function* () {
|
|
566
610
|
var _a;
|
|
567
|
-
|
|
568
|
-
"Wxid": userName,
|
|
569
|
-
"Scene": 0,
|
|
570
|
-
"Synckey": ""
|
|
571
|
-
});
|
|
572
|
-
const data = resp.data;
|
|
573
|
-
if (!(yield this.checkSyncLoginStatus(data, userName))) {
|
|
611
|
+
if (!(yield this.checkSyncLoginStatus(data, userName)))
|
|
574
612
|
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();
|
|
613
|
+
if (!data.Success || !((_a = data.Data) === null || _a === void 0 ? void 0 : _a.AddMsgs))
|
|
614
|
+
return;
|
|
615
|
+
let failed = false;
|
|
616
|
+
for (const msg of data.Data.AddMsgs) {
|
|
617
|
+
try {
|
|
618
|
+
// One malformed entry must not discard unrelated messages in this consumed batch.
|
|
619
|
+
(0, CallbackPayload_1.validateRawMessage)(msg);
|
|
620
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
621
|
+
try {
|
|
622
|
+
yield this.processMessage(userName, msg);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
if (attempt === 2)
|
|
627
|
+
throw error;
|
|
628
|
+
yield new Promise(resolve => setTimeout(resolve, 100));
|
|
619
629
|
}
|
|
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
630
|
}
|
|
644
631
|
}
|
|
632
|
+
catch (_b) {
|
|
633
|
+
failed = true;
|
|
634
|
+
console.error('单条微信消息处理失败,其余消息继续处理');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (failed)
|
|
638
|
+
throw new Error('Some messages could not be processed');
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
processMessage(userName, msg) {
|
|
642
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
643
|
+
var _a, _b;
|
|
644
|
+
const now = Date.now();
|
|
645
|
+
for (const [key, seenAt] of this.recentMsgIds) {
|
|
646
|
+
if (now - seenAt < 120000)
|
|
647
|
+
break;
|
|
648
|
+
this.recentMsgIds.delete(key);
|
|
649
|
+
}
|
|
650
|
+
const newMsgId = String(msg.NewMsgId);
|
|
651
|
+
const identity = newMsgId !== '0' ? newMsgId
|
|
652
|
+
: `${msg.MsgId}:${msg.FromUserName.string}:${msg.ToUserName.string}:${msg.CreateTime}`;
|
|
653
|
+
const key = `${userName}:${identity}`;
|
|
654
|
+
if (this.recentMsgIds.has(key))
|
|
655
|
+
return;
|
|
656
|
+
// 将msg转换为Message类型
|
|
657
|
+
const message = new Message_1.Message({
|
|
658
|
+
MsgId: msg.MsgId,
|
|
659
|
+
FromUserName: msg.FromUserName.string.endsWith('@chatroom') ? msg.Content.string.split(':\n')[0] : msg.FromUserName.string,
|
|
660
|
+
ToUserName: msg.FromUserName.string.endsWith('@chatroom') ? msg.FromUserName.string : msg.ToUserName.string,
|
|
661
|
+
MsgType: msg.MsgType,
|
|
662
|
+
Content: msg.FromUserName.string.endsWith('@chatroom') ? msg.Content.string.split(':\n').slice(1).join(':\n') : msg.Content.string,
|
|
663
|
+
Status: msg.Status,
|
|
664
|
+
ImgStatus: msg.ImgStatus,
|
|
665
|
+
ImgBuf: ((_a = msg.ImgBuf) === null || _a === void 0 ? void 0 : _a.iLen) || 0,
|
|
666
|
+
CreateTime: msg.CreateTime,
|
|
667
|
+
MsgSource: msg.MsgSource,
|
|
668
|
+
NewMsgId: newMsgId,
|
|
669
|
+
MsgSeq: msg.MsgSeq,
|
|
670
|
+
RoomId: (msg.FromUserName.string.endsWith('@chatroom') ? msg.FromUserName.string : '') || (msg.ToUserName.string.endsWith('@chatroom') ? msg.ToUserName.string : ''),
|
|
671
|
+
IsSelf: msg.FromUserName.string === userName,
|
|
672
|
+
xml: msg.Content.string
|
|
673
|
+
});
|
|
674
|
+
// 处理企业微信
|
|
675
|
+
if ((_b = message.fromId) === null || _b === void 0 ? void 0 : _b.includes('@openim')) {
|
|
676
|
+
let contactE = yield contact_service_1.contactService.getByUserName(message.fromId);
|
|
677
|
+
if (!contactE) {
|
|
678
|
+
contactE = new ContactEntity_1.ContactEntity();
|
|
679
|
+
}
|
|
680
|
+
contactE.nickName = msg.PushContent.split(':')[0].slice(0, -1);
|
|
681
|
+
contactE.userName = message.fromId;
|
|
682
|
+
contactE.smallHeadImgUrl = 'https://raw.githubusercontent.com/finalpi/wechat2tg/wx2tg-pad/qywx.jpg';
|
|
683
|
+
contactE.bigHeadImgUrl = 'https://raw.githubusercontent.com/finalpi/wechat2tg/wx2tg-pad/qywx.jpg';
|
|
684
|
+
yield contact_service_1.contactService.upsertByUsername(contactE.userName, contactE);
|
|
685
|
+
}
|
|
686
|
+
if (message.type() === MessageType_1.MessageType.AddFriend) { // 好友请求
|
|
687
|
+
const obj = Message_1.Message.getXmlToJson(message.text());
|
|
688
|
+
const friendship = new Friendship_1.Friendship({
|
|
689
|
+
fromnickname: obj.msg.fromnickname,
|
|
690
|
+
v3: obj.msg.encryptusername,
|
|
691
|
+
v4: obj.msg.ticket,
|
|
692
|
+
scene: obj.msg.scene,
|
|
693
|
+
fromusername: obj.msg.fromusername,
|
|
694
|
+
bigHeadImgUrl: obj.msg.bigHeadImgUrl,
|
|
695
|
+
smallHeadImgUrl: obj.msg.smallHeadImgUrl,
|
|
696
|
+
content: obj.msg.content
|
|
697
|
+
});
|
|
698
|
+
this.emit('friendship', friendship);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
this.emit('message', message);
|
|
702
|
+
}
|
|
703
|
+
// Mark only after successful dispatch so a synchronous failure can be retried.
|
|
704
|
+
this.recentMsgIds.set(key, now);
|
|
705
|
+
if (this.recentMsgIds.size > 10000) {
|
|
706
|
+
this.recentMsgIds.delete(this.recentMsgIds.keys().next().value);
|
|
645
707
|
}
|
|
646
708
|
});
|
|
647
709
|
}
|
|
@@ -713,6 +775,7 @@ class WxBot extends events_1.EventEmitter {
|
|
|
713
775
|
});
|
|
714
776
|
if (resp.data.Code === 0) {
|
|
715
777
|
yield this.loginSuccess(config.wxid);
|
|
778
|
+
yield this.enqueueMessages(config.wxid, resp.data);
|
|
716
779
|
}
|
|
717
780
|
else {
|
|
718
781
|
yield this.login();
|
|
@@ -742,6 +805,7 @@ class WxBot extends events_1.EventEmitter {
|
|
|
742
805
|
yield LoginApi_1.loginApi.LoginTwiceAutoAuth(config === null || config === void 0 ? void 0 : config.wxid);
|
|
743
806
|
if (yield this.checkSyncLoginStatus(resp.data, config === null || config === void 0 ? void 0 : config.wxid)) {
|
|
744
807
|
yield this.loginSuccess(config.wxid);
|
|
808
|
+
yield this.enqueueMessages(config.wxid, resp.data);
|
|
745
809
|
return true;
|
|
746
810
|
}
|
|
747
811
|
else {
|
|
@@ -769,3 +833,6 @@ class WxBot extends events_1.EventEmitter {
|
|
|
769
833
|
}
|
|
770
834
|
}
|
|
771
835
|
exports.WxBot = WxBot;
|
|
836
|
+
var ContactClassification_1 = require("./utils/ContactClassification");
|
|
837
|
+
Object.defineProperty(exports, "normalizeContactVerifyFlag", { enumerable: true, get: function () { return ContactClassification_1.normalizeContactVerifyFlag; } });
|
|
838
|
+
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
|
+
}
|