wx2tg-puppet 1.0.7 → 1.0.9

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.
@@ -59,6 +59,32 @@ const audioUtil_1 = require("../utils/audioUtil");
59
59
  const path_1 = __importDefault(require("path"));
60
60
  const common_2 = require("../action/common");
61
61
  const fs = __importStar(require("fs"));
62
+ const FileChunkHelper_1 = require("../utils/FileChunkHelper");
63
+ const MAX_RETRIES = 3;
64
+ const RETRY_DELAY = 1000; // 1 second
65
+ // @ts-ignore - Ignore type errors for the entire function since we're dealing with a mix of old and new axios types
66
+ function retryDownload(downloadFn) {
67
+ return __awaiter(this, void 0, void 0, function* () {
68
+ var _a, _b, _c, _d, _e, _f;
69
+ let lastError;
70
+ for (let i = 0; i < MAX_RETRIES; i++) {
71
+ try {
72
+ const response = yield downloadFn();
73
+ if (((_c = (_b = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.Data) === null || _b === void 0 ? void 0 : _b.BaseResponse) === null || _c === void 0 ? void 0 : _c.ret) === 0) {
74
+ return response;
75
+ }
76
+ lastError = new Error(`Download failed with ret code: ${(_f = (_e = (_d = response === null || response === void 0 ? void 0 : response.data) === null || _d === void 0 ? void 0 : _d.Data) === null || _e === void 0 ? void 0 : _e.BaseResponse) === null || _f === void 0 ? void 0 : _f.ret}`);
77
+ }
78
+ catch (error) {
79
+ lastError = error;
80
+ }
81
+ if (i < MAX_RETRIES - 1) {
82
+ yield new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
83
+ }
84
+ }
85
+ throw new Error(`Download failed after ${MAX_RETRIES} attempts. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`);
86
+ });
87
+ }
62
88
  /**
63
89
  * Message class for handling and interacting with messages in cache.
64
90
  */
@@ -319,39 +345,117 @@ class Message {
319
345
  const xml = this.msg.Content;
320
346
  const json = Message.getXmlToJson(xml);
321
347
  const config = yield config_service_1.configService.get();
322
- const resp = yield ToolsApi_1.toolsApi.DownloadImg({
348
+ const totalSize = parseInt(json.msg.img.length);
349
+ // Get initial chunk size from first request
350
+ const firstChunk = FileChunkHelper_1.FileChunkHelper.getChunk(totalSize, 0);
351
+ if (!firstChunk)
352
+ return null;
353
+ // @ts-ignore - Using any type for response due to mixed axios versions
354
+ const firstResp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadImg({
323
355
  "CompressType": 0,
324
- "DataLen": parseInt(json.msg.img.length),
356
+ "DataLen": totalSize,
325
357
  "MsgId": this.msg.MsgId,
326
- "Section": {
327
- "DataLen": parseInt(json.msg.img.length),
328
- "StartPos": 0
329
- },
358
+ "Section": firstChunk,
330
359
  "ToWxid": this.msg.ToUserName,
331
360
  "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
332
- });
333
- const base64Data = resp.data.Data.data.buffer;
334
- // 你可以给图片起个名字,比如用 MsgId
335
- const fileBox = file_box_1.FileBox.fromBase64(base64Data, `${this.msg.MsgId}.png`);
361
+ }));
362
+ const downloadedLength = firstResp.data.Data.data.iLen;
363
+ const chunks = FileChunkHelper_1.FileChunkHelper.calculateChunks(totalSize, downloadedLength);
364
+ let completeBuffer = Buffer.from(firstResp.data.Data.data.buffer, 'base64');
365
+ // Start from second chunk since we already have the first one
366
+ for (let i = 1; i < chunks.length; i++) {
367
+ const chunk = chunks[i];
368
+ // @ts-ignore - Using any type for response due to mixed axios versions
369
+ const resp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadImg({
370
+ "CompressType": 0,
371
+ "DataLen": totalSize,
372
+ "MsgId": this.msg.MsgId,
373
+ "Section": chunk,
374
+ "ToWxid": this.msg.ToUserName,
375
+ "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
376
+ }));
377
+ const chunkData = Buffer.from(resp.data.Data.data.buffer, 'base64');
378
+ completeBuffer = Buffer.concat([completeBuffer, chunkData]);
379
+ }
380
+ const fileBox = file_box_1.FileBox.fromBuffer(completeBuffer, `${this.msg.MsgId}.png`);
336
381
  return fileBox;
337
382
  }
338
383
  else if (this.type() === MessageType_1.MessageType.Video) {
339
384
  const xml = this.msg.Content;
340
385
  const json = Message.getXmlToJson(xml);
341
386
  const config = yield config_service_1.configService.get();
342
- const resp = yield ToolsApi_1.toolsApi.DownloadVideo({
387
+ const totalSize = parseInt(json.msg.videomsg.length);
388
+ // Get initial chunk size from first request
389
+ const firstChunk = FileChunkHelper_1.FileChunkHelper.getChunk(totalSize, 0);
390
+ if (!firstChunk)
391
+ return null;
392
+ // @ts-ignore - Using any type for response due to mixed axios versions
393
+ const firstResp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadVideo({
343
394
  "CompressType": 0,
344
- "DataLen": parseInt(json.msg.videomsg.length),
395
+ "DataLen": totalSize,
345
396
  "MsgId": this.msg.MsgId,
346
- "Section": {
347
- "DataLen": parseInt(json.msg.videomsg.length),
348
- "StartPos": 0
349
- },
397
+ "Section": firstChunk,
350
398
  "ToWxid": this.msg.ToUserName,
351
399
  "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
352
- });
353
- const base64Data = resp.data.Data.data.buffer;
354
- const fileBox = file_box_1.FileBox.fromBase64(base64Data, `${this.msg.MsgId}.mp4`);
400
+ }));
401
+ const downloadedLength = firstResp.data.Data.data.iLen;
402
+ const chunks = FileChunkHelper_1.FileChunkHelper.calculateChunks(totalSize, downloadedLength);
403
+ let completeBuffer = Buffer.from(firstResp.data.Data.data.buffer, 'base64');
404
+ // Start from second chunk since we already have the first one
405
+ for (let i = 1; i < chunks.length; i++) {
406
+ const chunk = chunks[i];
407
+ // @ts-ignore - Using any type for response due to mixed axios versions
408
+ const resp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadVideo({
409
+ "CompressType": 0,
410
+ "DataLen": totalSize,
411
+ "MsgId": this.msg.MsgId,
412
+ "Section": chunk,
413
+ "ToWxid": this.msg.ToUserName,
414
+ "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
415
+ }));
416
+ const chunkData = Buffer.from(resp.data.Data.data.buffer, 'base64');
417
+ completeBuffer = Buffer.concat([completeBuffer, chunkData]);
418
+ }
419
+ const fileBox = file_box_1.FileBox.fromBuffer(completeBuffer, `${this.msg.MsgId}.mp4`);
420
+ return fileBox;
421
+ }
422
+ else if (this.type() === MessageType_1.MessageType.File) {
423
+ const xml = this.msg.Content;
424
+ const json = Message.getXmlToJson(xml);
425
+ const config = yield config_service_1.configService.get();
426
+ const totalSize = parseInt(json.msg.appmsg.appattach.totallen);
427
+ // Get initial chunk size from first request
428
+ const firstChunk = FileChunkHelper_1.FileChunkHelper.getChunk(totalSize, 0);
429
+ if (!firstChunk)
430
+ return null;
431
+ // @ts-ignore - Using any type for response due to mixed axios versions
432
+ const firstResp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadFile({
433
+ "AppID": json.msg.appmsg.appid,
434
+ "AttachId": json.msg.appmsg.appattach.attachid,
435
+ "DataLen": totalSize,
436
+ "Section": firstChunk,
437
+ "UserName": this.msg.FromUserName,
438
+ "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
439
+ }));
440
+ const downloadedLength = firstResp.data.Data.data.iLen;
441
+ const chunks = FileChunkHelper_1.FileChunkHelper.calculateChunks(totalSize, downloadedLength);
442
+ let completeBuffer = Buffer.from(firstResp.data.Data.data.buffer, 'base64');
443
+ // Start from second chunk since we already have the first one
444
+ for (let i = 1; i < chunks.length; i++) {
445
+ const chunk = chunks[i];
446
+ // @ts-ignore - Using any type for response due to mixed axios versions
447
+ const resp = yield retryDownload(() => ToolsApi_1.toolsApi.DownloadFile({
448
+ "AppID": json.msg.appmsg.appid,
449
+ "AttachId": json.msg.appmsg.appattach.attachid,
450
+ "DataLen": totalSize,
451
+ "Section": chunk,
452
+ "UserName": this.msg.FromUserName,
453
+ "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
454
+ }));
455
+ const chunkData = Buffer.from(resp.data.Data.data.buffer, 'base64');
456
+ completeBuffer = Buffer.concat([completeBuffer, chunkData]);
457
+ }
458
+ const fileBox = file_box_1.FileBox.fromBuffer(completeBuffer, json.msg.appmsg.title);
355
459
  return fileBox;
356
460
  }
357
461
  else if (this.type() === MessageType_1.MessageType.Voice) {
@@ -369,35 +473,12 @@ class Message {
369
473
  const silkFileBox = file_box_1.FileBox.fromBase64(base64Data, `${this.msg.MsgId}.silk`);
370
474
  const silkFilePath = path_1.default.resolve(__dirname, `${this.msg.MsgId}.silk`);
371
475
  const mp3FilePath = path_1.default.resolve(__dirname, `${this.msg.MsgId}.mp3`);
372
- // 保存 Silk 文件到本地
373
476
  yield silkFileBox.toFile(silkFilePath);
374
- // 使用 SilkToMp3Converter 转换为 MP3
375
477
  const converter = new audioUtil_1.SilkToMp3Converter();
376
478
  yield converter.convertToMp3(silkFilePath, mp3FilePath);
377
- // 删除 Silk 文件,只保留 MP3
378
479
  fs.unlinkSync(silkFilePath);
379
- // 返回 MP3 文件的 FileBox
380
480
  return file_box_1.FileBox.fromFile(mp3FilePath);
381
481
  }
382
- else if (this.type() === MessageType_1.MessageType.File) {
383
- const xml = this.msg.Content;
384
- const json = Message.getXmlToJson(xml);
385
- const config = yield config_service_1.configService.get();
386
- const resp = yield ToolsApi_1.toolsApi.DownloadFile({
387
- "AppID": json.msg.appmsg.appid,
388
- "AttachId": json.msg.appmsg.appattach.attachid,
389
- "DataLen": json.msg.appmsg.appattach.totallen,
390
- "Section": {
391
- "DataLen": json.msg.appmsg.appattach.totallen,
392
- "StartPos": 0
393
- },
394
- "UserName": this.msg.FromUserName,
395
- "Wxid": (config === null || config === void 0 ? void 0 : config.wxid) || ''
396
- });
397
- const base64Data = resp.data.Data.data.buffer;
398
- const fileBox = file_box_1.FileBox.fromBase64(base64Data, json.msg.appmsg.title);
399
- return fileBox;
400
- }
401
482
  else if (this.type() === MessageType_1.MessageType.Emoji) {
402
483
  const xml = this.msg.Content;
403
484
  const json = Message.getXmlToJson(xml);
@@ -2,4 +2,6 @@ export declare class Config {
2
2
  id: number;
3
3
  uuid: string;
4
4
  wxid: string;
5
+ deviceId: string;
6
+ DeviceName: string;
5
7
  }
@@ -26,6 +26,14 @@ __decorate([
26
26
  (0, typeorm_1.Column)({ nullable: true }),
27
27
  __metadata("design:type", String)
28
28
  ], Config.prototype, "wxid", void 0);
29
+ __decorate([
30
+ (0, typeorm_1.Column)({ nullable: true }),
31
+ __metadata("design:type", String)
32
+ ], Config.prototype, "deviceId", void 0);
33
+ __decorate([
34
+ (0, typeorm_1.Column)({ nullable: true }),
35
+ __metadata("design:type", String)
36
+ ], Config.prototype, "DeviceName", void 0);
29
37
  exports.Config = Config = __decorate([
30
38
  (0, typeorm_1.Entity)()
31
39
  ], Config);
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export declare class WxBot extends EventEmitter {
20
20
  private interval;
21
21
  private heartbeatInterval;
22
22
  private msgInterval;
23
+ private recentMsgIds;
23
24
  constructor({ baseUrl, databasePath }: WxBotOptions);
24
25
  private fetchAndShowQRCode;
25
26
  private setupHeartbeat;
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@ class WxBot extends events_1.EventEmitter {
43
43
  this.interval = null;
44
44
  this.heartbeatInterval = null;
45
45
  this.msgInterval = null;
46
+ this.recentMsgIds = []; // Store recent message IDs
46
47
  this.baseUrl = baseUrl;
47
48
  this.databasePath = databasePath;
48
49
  (0, axiosInstance_1.setBaseUrl)(baseUrl);
@@ -50,9 +51,10 @@ class WxBot extends events_1.EventEmitter {
50
51
  fetchAndShowQRCode() {
51
52
  return __awaiter(this, void 0, void 0, function* () {
52
53
  try {
54
+ const config = yield config_service_1.configService.get();
53
55
  const response = yield LoginApi_1.loginApi.GetQR({
54
- "DeviceID": "",
55
- "DeviceName": "",
56
+ "DeviceID": (config === null || config === void 0 ? void 0 : config.deviceId) || "",
57
+ "DeviceName": (config === null || config === void 0 ? void 0 : config.DeviceName) || "",
56
58
  "OSModel": "",
57
59
  "Proxy": {
58
60
  "ProxyIp": "",
@@ -106,7 +108,10 @@ class WxBot extends events_1.EventEmitter {
106
108
  const statusData = statusResp.data;
107
109
  if (statusData.Success) {
108
110
  if (statusData.Message === '登陆成功') {
109
- yield config_service_1.configService.save(this.uuid, statusData.Data.acctSectResp.userName);
111
+ yield config_service_1.configService.save({
112
+ uuid: this.uuid,
113
+ wxid: statusData.Data.acctSectResp.userName,
114
+ });
110
115
  this.loginSuccess(statusData.Data.acctSectResp.userName);
111
116
  if (this.interval)
112
117
  clearInterval(this.interval);
@@ -164,6 +169,14 @@ class WxBot extends events_1.EventEmitter {
164
169
  return;
165
170
  }
166
171
  console.log('好友列表获取成功,开始更新联系人信息');
172
+ // 设置设备信息
173
+ const cacheInfo = yield LoginApi_1.loginApi.GetCacheInfo(userName);
174
+ if (cacheInfo.data.Message === '成功') {
175
+ yield config_service_1.configService.save({
176
+ deviceId: cacheInfo.data.Data.Deviceid_str,
177
+ DeviceName: cacheInfo.data.Data.DeviceName,
178
+ });
179
+ }
167
180
  // 获取详情
168
181
  const data = res.data;
169
182
  data.Data.ContactUsernameList.forEach((contact) => {
@@ -191,6 +204,16 @@ class WxBot extends events_1.EventEmitter {
191
204
  // 更新联系人或者群组信息
192
205
  // 新增:触发 message 事件
193
206
  for (const msg of data.Data.AddMsgs) {
207
+ // Check for duplicate messages
208
+ const newMsgId = BigInt(msg.NewMsgId);
209
+ if (this.recentMsgIds.includes(newMsgId)) {
210
+ continue; // Skip duplicate message
211
+ }
212
+ // Add to recent messages and maintain only last 10
213
+ this.recentMsgIds.push(newMsgId);
214
+ if (this.recentMsgIds.length > 10) {
215
+ this.recentMsgIds.shift(); // Remove oldest message ID
216
+ }
194
217
  // 将msg转换为Message类型
195
218
  const message = new Message_1.Message({
196
219
  MsgId: msg.MsgId,
@@ -2,7 +2,7 @@ import { Config } from '../entities/config.entity';
2
2
  declare class ConfigService {
3
3
  private get repo();
4
4
  get(): Promise<Config | null>;
5
- save(uuid: string, wxid: string): Promise<Config>;
5
+ save(config: Partial<Config>): Promise<Config>;
6
6
  }
7
7
  export declare const configService: ConfigService;
8
8
  export {};
@@ -21,17 +21,17 @@ class ConfigService {
21
21
  return this.repo.findOne({ where: {} });
22
22
  });
23
23
  }
24
- save(uuid, wxid) {
24
+ save(config) {
25
25
  return __awaiter(this, void 0, void 0, function* () {
26
- let config = yield this.get();
27
- if (config) {
28
- config.uuid = uuid;
29
- config.wxid = wxid;
26
+ let existingConfig = yield this.get();
27
+ if (existingConfig) {
28
+ Object.assign(existingConfig, config);
29
+ return this.repo.save(existingConfig);
30
30
  }
31
31
  else {
32
- config = this.repo.create({ uuid, wxid });
32
+ const newConfig = this.repo.create(config);
33
+ return this.repo.save(newConfig);
33
34
  }
34
- return this.repo.save(config);
35
35
  });
36
36
  }
37
37
  }
@@ -0,0 +1,30 @@
1
+ export interface FileChunk {
2
+ DataLen: number;
3
+ StartPos: number;
4
+ }
5
+ export declare class FileChunkHelper {
6
+ private static readonly INITIAL_CHUNK_SIZE;
7
+ private static lastDownloadedLength;
8
+ /**
9
+ * Calculate file chunks based on total file size
10
+ * @param totalSize Total file size in bytes
11
+ * @param downloadedLength Optional length from previous download (iLen)
12
+ * @returns Array of chunks with DataLen and StartPos
13
+ */
14
+ static calculateChunks(totalSize: number, downloadedLength?: number): FileChunk[];
15
+ /**
16
+ * Get a specific chunk information
17
+ * @param totalSize Total file size in bytes
18
+ * @param chunkIndex Chunk index (0-based)
19
+ * @param downloadedLength Optional length from previous download (iLen)
20
+ * @returns Chunk information or null if index is out of range
21
+ */
22
+ static getChunk(totalSize: number, chunkIndex: number, downloadedLength?: number): FileChunk | null;
23
+ /**
24
+ * Get total number of chunks needed for a file
25
+ * @param totalSize Total file size in bytes
26
+ * @param downloadedLength Optional length from previous download (iLen)
27
+ * @returns Total number of chunks
28
+ */
29
+ static getTotalChunks(totalSize: number, downloadedLength?: number): number;
30
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileChunkHelper = void 0;
4
+ class FileChunkHelper {
5
+ /**
6
+ * Calculate file chunks based on total file size
7
+ * @param totalSize Total file size in bytes
8
+ * @param downloadedLength Optional length from previous download (iLen)
9
+ * @returns Array of chunks with DataLen and StartPos
10
+ */
11
+ static calculateChunks(totalSize, downloadedLength) {
12
+ const chunks = [];
13
+ let position = 0;
14
+ // Use consistent chunk size throughout
15
+ const chunkSize = downloadedLength || this.INITIAL_CHUNK_SIZE;
16
+ while (position < totalSize) {
17
+ const remainingSize = totalSize - position;
18
+ const currentChunkSize = Math.min(chunkSize, remainingSize);
19
+ chunks.push({
20
+ DataLen: currentChunkSize,
21
+ StartPos: position
22
+ });
23
+ position += currentChunkSize;
24
+ }
25
+ return chunks;
26
+ }
27
+ /**
28
+ * Get a specific chunk information
29
+ * @param totalSize Total file size in bytes
30
+ * @param chunkIndex Chunk index (0-based)
31
+ * @param downloadedLength Optional length from previous download (iLen)
32
+ * @returns Chunk information or null if index is out of range
33
+ */
34
+ static getChunk(totalSize, chunkIndex, downloadedLength) {
35
+ const chunks = this.calculateChunks(totalSize, downloadedLength);
36
+ return chunks[chunkIndex] || null;
37
+ }
38
+ /**
39
+ * Get total number of chunks needed for a file
40
+ * @param totalSize Total file size in bytes
41
+ * @param downloadedLength Optional length from previous download (iLen)
42
+ * @returns Total number of chunks
43
+ */
44
+ static getTotalChunks(totalSize, downloadedLength) {
45
+ return this.calculateChunks(totalSize, downloadedLength).length;
46
+ }
47
+ }
48
+ exports.FileChunkHelper = FileChunkHelper;
49
+ FileChunkHelper.INITIAL_CHUNK_SIZE = 1024; // Initial chunk size for first request
50
+ FileChunkHelper.lastDownloadedLength = null; // Store the last downloaded length
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wx2tg-puppet",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [