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/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 = []; // Store recent message IDs
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.forEach((contact) => {
343
- if (!contact.endsWith('@chatroom')) {
344
- Contact_1.Contact.fetchAndUpsertContactDetail(userName, contact);
345
- }
346
- else {
347
- Room_1.Room.find({ id: contact });
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
- while (true) {
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.handleCallbackMessage(userName);
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
- if (this.httpServer) {
418
- this.httpServer.close();
419
- this.httpServer = null;
420
- }
421
- this.httpServer = http.createServer((req, res) => {
422
- // 设置CORS头
423
- res.setHeader('Access-Control-Allow-Origin', '*');
424
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
425
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
426
- if (req.method === 'OPTIONS') {
427
- res.writeHead(200);
428
- res.end();
429
- return;
430
- }
431
- const url = req.url || '';
432
- const callbackPattern = `/message/callback/${userName}`;
433
- if (req.method === 'POST' && url === callbackPattern) {
434
- let body = '';
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
- chunks.push(chunk);
438
- body += chunk.toString('utf8');
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
- this.handleCallbackMessage(userName);
443
- res.writeHead(200, { 'Content-Type': 'application/json' });
444
- res.end(JSON.stringify({ success: true }));
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 (error) {
447
- console.error('JSON解析错误:', error);
448
- console.error('原始内容:', body);
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
- req.on('error', (error) => {
454
- console.error('请求处理错误:', error);
455
- res.writeHead(500, { 'Content-Type': 'application/json' });
456
- res.end(JSON.stringify({ error: 'Request error' }));
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
- res.writeHead(404, { 'Content-Type': 'application/json' });
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(() => __awaiter(this, void 0, void 0, function* () {
484
- yield this.handleMsg(userName);
485
- }), 1000);
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
- if (this.syncInProgress) {
553
- return;
554
- }
555
- this.syncInProgress = true;
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.handleMsgOnce(userName);
594
+ yield this.syncTask;
558
595
  }
559
596
  finally {
560
- this.syncInProgress = false;
597
+ this.syncTask = null;
561
598
  }
562
599
  });
563
600
  }
564
- handleMsgOnce(userName) {
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
- const resp = yield MsgApi_1.msgApi.Sync({
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
- if (data.Success) {
577
- if (data.Message === '当前未有新消息') {
578
- return;
579
- }
580
- if (!data.Data.AddMsgs) {
581
- return;
582
- }
583
- // 更新联系人或者群组信息
584
- // 新增:触发 message 事件
585
- for (const msg of data.Data.AddMsgs) {
586
- // Check for duplicate messages
587
- const newMsgId = BigInt(msg.NewMsgId);
588
- if (this.recentMsgIds.includes(newMsgId)) {
589
- continue; // Skip duplicate message
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
- const entity = yield this.getByUserName(userName);
39
- if (!entity)
40
- return null;
41
- Object.assign(entity, update);
42
- return this.repo.save(entity);
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
- let contact = yield this.getByUserName(userName);
60
- if (contact) {
61
- Object.assign(contact, data);
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) {
@@ -15,6 +15,7 @@ export declare const MessageType: {
15
15
  Link: string;
16
16
  AddFriend: string;
17
17
  Quote: string;
18
+ Solitaire: string;
18
19
  Transfer: string;
19
20
  RedPacket: string;
20
21
  VideoAccount: string;
@@ -18,6 +18,7 @@ exports.MessageType = {
18
18
  Link: 'link', // 公众号链接
19
19
  AddFriend: 'add_friend', // 添加好友通知
20
20
  Quote: 'quote', // 引用消息
21
+ Solitaire: 'solitaire', // 接龙
21
22
  Transfer: 'transfer', //转账
22
23
  RedPacket: 'red_packet', //红包
23
24
  VideoAccount: 'video_account', // 视频号消息
@@ -0,0 +1,4 @@
1
+ export declare const MAX_CALLBACK_BYTES: number;
2
+ export declare function validateSyncResponse(data: any): void;
3
+ export declare function validateRawMessage(msg: any): void;
4
+ export declare function parseCallbackPayload(body: string): any | null;
@@ -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
+ }