wechaty-puppet-matrix 0.0.53 → 0.0.55
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/cjs/src/matrix/messages/message-image.d.ts +8 -0
- package/dist/cjs/src/matrix/messages/message-image.d.ts.map +1 -1
- package/dist/cjs/src/matrix/messages/message-image.js +38 -2
- package/dist/cjs/src/matrix/messages/message-image.spec.d.ts +3 -0
- package/dist/cjs/src/matrix/messages/message-image.spec.d.ts.map +1 -0
- package/dist/cjs/src/matrix/messages/message-image.spec.js +66 -0
- package/dist/cjs/src/matrix/messages/message-video.d.ts +6 -0
- package/dist/cjs/src/matrix/messages/message-video.d.ts.map +1 -1
- package/dist/cjs/src/matrix/messages/message-video.js +20 -0
- package/dist/cjs/src/matrix/messages/message-video.spec.d.ts +3 -0
- package/dist/cjs/src/matrix/messages/message-video.spec.d.ts.map +1 -0
- package/dist/cjs/src/matrix/messages/message-video.spec.js +27 -0
- package/dist/cjs/src/matrix/service/request.d.ts +9 -0
- package/dist/cjs/src/matrix/service/request.d.ts.map +1 -1
- package/dist/cjs/src/matrix/service/request.js +76 -42
- package/dist/cjs/src/matrix/utils/file-type.d.ts +4 -0
- package/dist/cjs/src/matrix/utils/file-type.d.ts.map +1 -0
- package/dist/cjs/src/matrix/utils/file-type.js +55 -0
- package/dist/cjs/src/matrix/utils/file-type.spec.d.ts +3 -0
- package/dist/cjs/src/matrix/utils/file-type.spec.d.ts.map +1 -0
- package/dist/cjs/src/matrix/utils/file-type.spec.js +25 -0
- package/dist/cjs/src/puppet-matrix.d.ts +1 -0
- package/dist/cjs/src/puppet-matrix.d.ts.map +1 -1
- package/dist/cjs/src/puppet-matrix.js +17 -2
- package/dist/esm/src/matrix/messages/message-image.d.ts +8 -0
- package/dist/esm/src/matrix/messages/message-image.d.ts.map +1 -1
- package/dist/esm/src/matrix/messages/message-image.js +37 -2
- package/dist/esm/src/matrix/messages/message-image.spec.d.ts +3 -0
- package/dist/esm/src/matrix/messages/message-image.spec.d.ts.map +1 -0
- package/dist/esm/src/matrix/messages/message-image.spec.js +64 -0
- package/dist/esm/src/matrix/messages/message-video.d.ts +6 -0
- package/dist/esm/src/matrix/messages/message-video.d.ts.map +1 -1
- package/dist/esm/src/matrix/messages/message-video.js +19 -0
- package/dist/esm/src/matrix/messages/message-video.spec.d.ts +3 -0
- package/dist/esm/src/matrix/messages/message-video.spec.d.ts.map +1 -0
- package/dist/esm/src/matrix/messages/message-video.spec.js +25 -0
- package/dist/esm/src/matrix/service/request.d.ts +9 -0
- package/dist/esm/src/matrix/service/request.d.ts.map +1 -1
- package/dist/esm/src/matrix/service/request.js +75 -42
- package/dist/esm/src/matrix/utils/file-type.d.ts +4 -0
- package/dist/esm/src/matrix/utils/file-type.d.ts.map +1 -0
- package/dist/esm/src/matrix/utils/file-type.js +47 -0
- package/dist/esm/src/matrix/utils/file-type.spec.d.ts +3 -0
- package/dist/esm/src/matrix/utils/file-type.spec.d.ts.map +1 -0
- package/dist/esm/src/matrix/utils/file-type.spec.js +23 -0
- package/dist/esm/src/puppet-matrix.d.ts +1 -0
- package/dist/esm/src/puppet-matrix.d.ts.map +1 -1
- package/dist/esm/src/puppet-matrix.js +17 -2
- package/package.json +1 -1
- package/src/matrix/messages/message-image.spec.ts +107 -0
- package/src/matrix/messages/message-image.ts +63 -2
- package/src/matrix/messages/message-video.spec.ts +37 -0
- package/src/matrix/messages/message-video.ts +28 -1
- package/src/matrix/service/request.ts +108 -42
- package/src/matrix/utils/file-type.spec.ts +29 -0
- package/src/matrix/utils/file-type.ts +59 -0
- package/src/puppet-matrix.ts +22 -3
|
@@ -97,6 +97,10 @@ export var WechatMessageType;
|
|
|
97
97
|
WechatMessageType[WechatMessageType["Sys"] = 10000] = "Sys";
|
|
98
98
|
WechatMessageType[WechatMessageType["SysTemplate"] = 10002] = "SysTemplate";
|
|
99
99
|
})(WechatMessageType || (WechatMessageType = {}));
|
|
100
|
+
export function parseSizeMismatch(errmsg) {
|
|
101
|
+
const match = typeof errmsg === 'string' ? errmsg.match(/size differs: expected \d+, got (\d+)/) : null;
|
|
102
|
+
return match && Number(match[1]) > 0 ? match[1] : undefined;
|
|
103
|
+
}
|
|
100
104
|
export var FriendshipType;
|
|
101
105
|
(function (FriendshipType) {
|
|
102
106
|
FriendshipType[FriendshipType["Unknown"] = 0] = "Unknown";
|
|
@@ -1349,8 +1353,7 @@ class Client extends EventEmitter {
|
|
|
1349
1353
|
},
|
|
1350
1354
|
});
|
|
1351
1355
|
}
|
|
1352
|
-
async retryDownload(fn) {
|
|
1353
|
-
const maxRetries = 5;
|
|
1356
|
+
async retryDownload(fn, maxRetries = 5) {
|
|
1354
1357
|
const delayMs = 5000;
|
|
1355
1358
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1356
1359
|
try {
|
|
@@ -1359,6 +1362,9 @@ class Client extends EventEmitter {
|
|
|
1359
1362
|
return res;
|
|
1360
1363
|
}
|
|
1361
1364
|
log.warn(PRE, 'download attempt %d/%d failed (no url in response): %s', attempt, maxRetries, JSON.stringify(res));
|
|
1365
|
+
if (parseSizeMismatch(res?.errmsg)) {
|
|
1366
|
+
return res;
|
|
1367
|
+
}
|
|
1362
1368
|
}
|
|
1363
1369
|
catch (e) {
|
|
1364
1370
|
log.warn(PRE, 'download attempt %d/%d error: %s', attempt, maxRetries, e);
|
|
@@ -1369,21 +1375,51 @@ class Client extends EventEmitter {
|
|
|
1369
1375
|
}
|
|
1370
1376
|
return null;
|
|
1371
1377
|
}
|
|
1378
|
+
async cdnDownloadWithFallback(label, baseParams, candidates) {
|
|
1379
|
+
const queue = [...candidates];
|
|
1380
|
+
const tried = new Set();
|
|
1381
|
+
const retriesPerCandidate = candidates.length > 1 ? 2 : 5;
|
|
1382
|
+
while (queue.length) {
|
|
1383
|
+
const candidate = queue.shift();
|
|
1384
|
+
const key = `${candidate.file_id}|${candidate.file_size}|${candidate.aes_key || ''}`;
|
|
1385
|
+
if (!candidate.file_id || tried.has(key)) {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
tried.add(key);
|
|
1389
|
+
const params = {
|
|
1390
|
+
...baseParams,
|
|
1391
|
+
file_id: candidate.file_id,
|
|
1392
|
+
file_size: candidate.file_size,
|
|
1393
|
+
...(candidate.aes_key ? { aes_key: candidate.aes_key } : {}),
|
|
1394
|
+
};
|
|
1395
|
+
const res = await this.retryDownload(() => this.postData({ path: '/cloud/cdn_download', data: params }), retriesPerCandidate);
|
|
1396
|
+
if (res?.url) {
|
|
1397
|
+
if (tried.size > 1) {
|
|
1398
|
+
log.info(PRE, '%s succeeded with fallback, file_size: %s', label, candidate.file_size);
|
|
1399
|
+
}
|
|
1400
|
+
return encodeURI(res.url) || '';
|
|
1401
|
+
}
|
|
1402
|
+
const actualSize = parseSizeMismatch(res?.errmsg);
|
|
1403
|
+
if (actualSize) {
|
|
1404
|
+
log.warn(PRE, '%s size mismatch (xml %s, actual %s), retry with actual size', label, candidate.file_size, actualSize);
|
|
1405
|
+
queue.unshift({ ...candidate, file_size: actualSize });
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
log.warn(PRE, '%s candidate failed, params: %s', label, JSON.stringify(params));
|
|
1409
|
+
}
|
|
1410
|
+
log.error(PRE, '%s failed for all candidates: %s', label, JSON.stringify(candidates));
|
|
1411
|
+
return '';
|
|
1412
|
+
}
|
|
1372
1413
|
async downloadImage(imageInfo) {
|
|
1373
|
-
const
|
|
1414
|
+
const candidates = imageInfo.candidates?.length
|
|
1415
|
+
? imageInfo.candidates
|
|
1416
|
+
: [{ file_id: imageInfo.file_id, file_size: imageInfo.file_size || '' }];
|
|
1417
|
+
return this.cdnDownloadWithFallback('downloadImage', {
|
|
1374
1418
|
log_response: true,
|
|
1375
1419
|
file_type: 2,
|
|
1376
|
-
file_id: imageInfo.file_id,
|
|
1377
1420
|
aes_key: imageInfo.aes_key,
|
|
1378
|
-
file_size: imageInfo.file_size,
|
|
1379
1421
|
file_name: `/微信图片_${format(new Date(), 'yyyyMMddHHmmss')}.png`,
|
|
1380
|
-
};
|
|
1381
|
-
const res = await this.retryDownload(() => this.postData({ path: '/cloud/cdn_download', data: params }));
|
|
1382
|
-
if (res?.url) {
|
|
1383
|
-
return encodeURI(res.url) || '';
|
|
1384
|
-
}
|
|
1385
|
-
log.error(PRE, 'downloadImage failed after %d retries, params: %s', 5, JSON.stringify(params));
|
|
1386
|
-
return '';
|
|
1422
|
+
}, candidates);
|
|
1387
1423
|
}
|
|
1388
1424
|
async downloadWWMedia(imageInfo, type) {
|
|
1389
1425
|
const typeNameMap = {
|
|
@@ -1407,20 +1443,12 @@ class Client extends EventEmitter {
|
|
|
1407
1443
|
return '';
|
|
1408
1444
|
}
|
|
1409
1445
|
async downloadFile(imageInfo) {
|
|
1410
|
-
|
|
1446
|
+
return this.cdnDownloadWithFallback('downloadFile', {
|
|
1411
1447
|
log_response: true,
|
|
1412
1448
|
file_type: 5,
|
|
1413
|
-
file_id: imageInfo.file_id.replace('@cdn_', ''),
|
|
1414
1449
|
aes_key: imageInfo.aes_key,
|
|
1415
|
-
file_size: imageInfo.file_size,
|
|
1416
1450
|
file_name: '/' + sanitizeFilename(imageInfo.file_name),
|
|
1417
|
-
};
|
|
1418
|
-
const res = await this.retryDownload(() => this.postData({ path: '/cloud/cdn_download', data: params }));
|
|
1419
|
-
if (res?.url) {
|
|
1420
|
-
return encodeURI(res.url) || '';
|
|
1421
|
-
}
|
|
1422
|
-
log.error(PRE, 'downloadFile failed after %d retries, params: %s', 5, JSON.stringify(params));
|
|
1423
|
-
return '';
|
|
1451
|
+
}, [{ file_id: imageInfo.file_id.replace('@cdn_', ''), file_size: imageInfo.file_size || '' }]);
|
|
1424
1452
|
}
|
|
1425
1453
|
async downloadAudio(msgId, voiceInfo, roomId) {
|
|
1426
1454
|
const params = {
|
|
@@ -1450,6 +1478,7 @@ class Client extends EventEmitter {
|
|
|
1450
1478
|
return await this.postData({ path: '/msg/get_voice_trans', data: { voice_id: voiceId } });
|
|
1451
1479
|
}
|
|
1452
1480
|
async voiceToText(msgId, length, timeoutMs = 20000) {
|
|
1481
|
+
const brief = (res) => JSON.stringify(res, (k, v) => (k === 'buffer' ? undefined : v));
|
|
1453
1482
|
try {
|
|
1454
1483
|
const idRes = await this.newVoiceTransId();
|
|
1455
1484
|
const voiceId = idRes?.voice_id;
|
|
@@ -1457,15 +1486,24 @@ class Client extends EventEmitter {
|
|
|
1457
1486
|
log.warn(PRE, 'voiceToText: no voice_id: %s', JSON.stringify(idRes));
|
|
1458
1487
|
return '';
|
|
1459
1488
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1489
|
+
let checkRes;
|
|
1490
|
+
for (let round = 0; round < 20; round++) {
|
|
1491
|
+
checkRes = await this.checkVoiceTrans(voiceId, msgId, length);
|
|
1492
|
+
log.verbose(PRE, 'voiceToText check#%d(length=%d): %s', round, length, brief(checkRes));
|
|
1493
|
+
if (checkRes?.baseResponse?.ret !== 0) {
|
|
1494
|
+
log.warn(PRE, 'voiceToText: check failed: %s', brief(checkRes));
|
|
1495
|
+
return '';
|
|
1496
|
+
}
|
|
1497
|
+
const ctx = checkRes?.uploadCtx;
|
|
1498
|
+
if (!ctx?.dataLen) {
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
const uploadRes = await this.uploadVoiceTrans(voiceId, msgId, length);
|
|
1502
|
+
log.verbose(PRE, 'voiceToText upload#%d: %s', round, brief(uploadRes));
|
|
1503
|
+
if (uploadRes?.baseResponse?.ret !== 0) {
|
|
1504
|
+
log.warn(PRE, 'voiceToText: upload failed: %s', brief(uploadRes));
|
|
1505
|
+
return '';
|
|
1506
|
+
}
|
|
1469
1507
|
}
|
|
1470
1508
|
const interval = (checkRes?.queryCtx?.interval || 2) * 1000;
|
|
1471
1509
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1473,7 +1511,7 @@ class Client extends EventEmitter {
|
|
|
1473
1511
|
await new Promise(resolve => setTimeout(resolve, interval));
|
|
1474
1512
|
const res = await this.getVoiceTrans(voiceId);
|
|
1475
1513
|
if (res?.baseResponse?.ret !== 0) {
|
|
1476
|
-
log.warn(PRE, 'voiceToText: get result failed: %s',
|
|
1514
|
+
log.warn(PRE, 'voiceToText: get result failed: %s', brief(res));
|
|
1477
1515
|
return '';
|
|
1478
1516
|
}
|
|
1479
1517
|
if (res?.transRes?.endFlag === 1) {
|
|
@@ -1488,19 +1526,14 @@ class Client extends EventEmitter {
|
|
|
1488
1526
|
return '';
|
|
1489
1527
|
}
|
|
1490
1528
|
async downloadVideo(videoInfo) {
|
|
1491
|
-
const
|
|
1529
|
+
const candidates = videoInfo.candidates?.length
|
|
1530
|
+
? videoInfo.candidates
|
|
1531
|
+
: [{ file_id: videoInfo.file_id, file_size: videoInfo.file_size }];
|
|
1532
|
+
return this.cdnDownloadWithFallback('downloadVideo', {
|
|
1492
1533
|
file_type: 4,
|
|
1493
|
-
file_id: videoInfo.file_id,
|
|
1494
1534
|
aes_key: videoInfo.aes_key,
|
|
1495
|
-
file_size: videoInfo.file_size,
|
|
1496
1535
|
file_name: `/微信视频_${format(new Date(), 'yyyyMMddHHmmss')}.mp4`,
|
|
1497
|
-
};
|
|
1498
|
-
const res = await this.retryDownload(() => this.postData({ path: '/cloud/cdn_download', data: params }));
|
|
1499
|
-
if (res?.url) {
|
|
1500
|
-
return encodeURI(res.url) || '';
|
|
1501
|
-
}
|
|
1502
|
-
log.error(PRE, 'downloadVideo failed after %d retries, params: %s', 5, JSON.stringify(params));
|
|
1503
|
-
return '';
|
|
1536
|
+
}, candidates);
|
|
1504
1537
|
}
|
|
1505
1538
|
async sendUrlImg(contactId, url, selfId) {
|
|
1506
1539
|
try {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function detectImageExt(head: Buffer): string | undefined;
|
|
2
|
+
export declare function fetchRemoteHead(url: string, bytes?: number, timeoutMs?: number): Promise<Buffer | undefined>;
|
|
3
|
+
export declare function replaceExt(fileName: string, ext: string): string;
|
|
4
|
+
//# sourceMappingURL=file-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-type.d.ts","sourceRoot":"","sources":["../../../../../src/matrix/utils/file-type.ts"],"names":[],"mappings":"AAKA,wBAAgB,cAAc,CAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA0BhE;AAKD,wBAAsB,eAAe,CAAE,GAAG,EAAE,MAAM,EAAE,KAAK,SAAK,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAa7G;AAKD,wBAAgB,UAAU,CAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIjE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
export function detectImageExt(head) {
|
|
3
|
+
if (head.length >= 3 && head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff) {
|
|
4
|
+
return 'jpg';
|
|
5
|
+
}
|
|
6
|
+
if (head.length >= 8 && head.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
|
|
7
|
+
return 'png';
|
|
8
|
+
}
|
|
9
|
+
if (head.length >= 6 && ['GIF87a', 'GIF89a'].includes(head.subarray(0, 6).toString('ascii'))) {
|
|
10
|
+
return 'gif';
|
|
11
|
+
}
|
|
12
|
+
if (head.length >= 12 && head.subarray(0, 4).toString('ascii') === 'RIFF' && head.subarray(8, 12).toString('ascii') === 'WEBP') {
|
|
13
|
+
return 'webp';
|
|
14
|
+
}
|
|
15
|
+
if (head.length >= 2 && head[0] === 0x42 && head[1] === 0x4d) {
|
|
16
|
+
return 'bmp';
|
|
17
|
+
}
|
|
18
|
+
if (head.length >= 12 && head.subarray(4, 8).toString('ascii') === 'ftyp') {
|
|
19
|
+
const brand = head.subarray(8, 12).toString('ascii');
|
|
20
|
+
if (['heic', 'heix', 'hevc', 'hevx', 'mif1', 'msf1'].includes(brand)) {
|
|
21
|
+
return 'heic';
|
|
22
|
+
}
|
|
23
|
+
if (['avif', 'avis'].includes(brand)) {
|
|
24
|
+
return 'avif';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
export async function fetchRemoteHead(url, bytes = 32, timeoutMs = 5000) {
|
|
30
|
+
try {
|
|
31
|
+
const res = await axios.get(url, {
|
|
32
|
+
headers: { Range: `bytes=0-${bytes - 1}` },
|
|
33
|
+
responseType: 'arraybuffer',
|
|
34
|
+
timeout: timeoutMs,
|
|
35
|
+
maxContentLength: 50 * 1024 * 1024,
|
|
36
|
+
});
|
|
37
|
+
return Buffer.from(res.data).subarray(0, bytes);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function replaceExt(fileName, ext) {
|
|
44
|
+
const dot = fileName.lastIndexOf('.');
|
|
45
|
+
const base = dot > 0 ? fileName.slice(0, dot) : fileName;
|
|
46
|
+
return `${base}.${ext}`;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-type.spec.d.ts","sourceRoot":"","sources":["../../../../../src/matrix/utils/file-type.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env -S node -r ts-node/register
|
|
2
|
+
import { test } from 'tstest';
|
|
3
|
+
import { detectImageExt, replaceExt } from './file-type.js';
|
|
4
|
+
const hex = (s) => Buffer.from(s, 'hex');
|
|
5
|
+
test('detectImageExt recognizes common image headers', async (t) => {
|
|
6
|
+
t.equal(detectImageExt(hex('ffd8fffe00104c61766335392e33372e')), 'jpg', 'jpeg');
|
|
7
|
+
t.equal(detectImageExt(hex('89504e470d0a1a0a0000000d49484452')), 'png', 'png');
|
|
8
|
+
t.equal(detectImageExt(Buffer.from('GIF89a......')), 'gif', 'gif');
|
|
9
|
+
t.equal(detectImageExt(Buffer.concat([Buffer.from('RIFF'), hex('00000000'), Buffer.from('WEBPVP8 ')])), 'webp', 'webp');
|
|
10
|
+
t.equal(detectImageExt(hex('424d000000000000')), 'bmp', 'bmp');
|
|
11
|
+
t.equal(detectImageExt(Buffer.concat([hex('00000018'), Buffer.from('ftypheic'), hex('00000000')])), 'heic', 'heic');
|
|
12
|
+
t.equal(detectImageExt(Buffer.concat([hex('0000001c'), Buffer.from('ftypavif'), hex('00000000')])), 'avif', 'avif');
|
|
13
|
+
});
|
|
14
|
+
test('detectImageExt returns undefined for unknown or short data', async (t) => {
|
|
15
|
+
t.equal(detectImageExt(hex('0000001c667479706d703432')), undefined, 'mp4 is not an image');
|
|
16
|
+
t.equal(detectImageExt(Buffer.from('<html>')), undefined, 'html');
|
|
17
|
+
t.equal(detectImageExt(Buffer.alloc(0)), undefined, 'empty');
|
|
18
|
+
});
|
|
19
|
+
test('replaceExt replaces or appends extension', async (t) => {
|
|
20
|
+
t.equal(replaceExt('微信图片_20261105234022.png', 'jpg'), '微信图片_20261105234022.jpg', 'replace');
|
|
21
|
+
t.equal(replaceExt('image', 'gif'), 'image.gif', 'append');
|
|
22
|
+
t.equal(replaceExt('.hidden', 'jpg'), '.hidden.jpg', 'dotfile');
|
|
23
|
+
});
|
|
@@ -126,6 +126,7 @@ declare class PuppetMatrix extends PUPPET.Puppet {
|
|
|
126
126
|
private _stopPuppetHeart;
|
|
127
127
|
private _fileBoxFromUrl;
|
|
128
128
|
private _getMessageImageFileBox;
|
|
129
|
+
private _imageNameByHead;
|
|
129
130
|
private _getMessageAudioFileBox;
|
|
130
131
|
private _getMessageVideoFileBox;
|
|
131
132
|
private _getMessageFileFileBox;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"puppet-matrix.d.ts","sourceRoot":"","sources":["../../../src/puppet-matrix.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,sBAAsB,CAAA;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAKhD,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAGxE,OAAO,MAAM,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"puppet-matrix.d.ts","sourceRoot":"","sources":["../../../src/puppet-matrix.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ import { WechatMessageType } from './matrix/types.js';
|
|
|
5
5
|
import Client from './matrix/service/request.js';
|
|
6
6
|
import { getUnixTime } from 'date-fns';
|
|
7
7
|
import { mergeWithFallback } from './matrix/utils/index.js';
|
|
8
|
+
import { detectImageExt, fetchRemoteHead, replaceExt } from './matrix/utils/file-type.js';
|
|
8
9
|
import path from 'path';
|
|
9
10
|
import { CacheManager } from './matrix/cache-manager.js';
|
|
10
11
|
import { isIMContactId, isRoomId } from './matrix/utils/is-type.js';
|
|
@@ -1288,7 +1289,7 @@ class PuppetMatrix extends PUPPET.Puppet {
|
|
|
1288
1289
|
}
|
|
1289
1290
|
try {
|
|
1290
1291
|
const audio = await parseAudioMessagePayload(message);
|
|
1291
|
-
const length = parseInt(audio.
|
|
1292
|
+
const length = parseInt(audio.length, 10);
|
|
1292
1293
|
if (!length) {
|
|
1293
1294
|
return;
|
|
1294
1295
|
}
|
|
@@ -1456,7 +1457,21 @@ class PuppetMatrix extends PUPPET.Puppet {
|
|
|
1456
1457
|
log.error(`Can not get file for message: ${messageId}`);
|
|
1457
1458
|
}
|
|
1458
1459
|
const url = imageInfo.is_ww ? await this._client?.downloadWWMedia(imageInfo, 1) : await this._client?.downloadImage(imageInfo);
|
|
1459
|
-
return this._fileBoxFromUrl(url, messageId);
|
|
1460
|
+
return this._fileBoxFromUrl(url, messageId, url ? await this._imageNameByHead(url) : undefined);
|
|
1461
|
+
}
|
|
1462
|
+
async _imageNameByHead(url) {
|
|
1463
|
+
const head = await fetchRemoteHead(url);
|
|
1464
|
+
const ext = head && detectImageExt(head);
|
|
1465
|
+
if (!ext) {
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
let rawName = 'image';
|
|
1469
|
+
try {
|
|
1470
|
+
rawName = decodeURIComponent(new URL(url).pathname.split('/').pop() || rawName);
|
|
1471
|
+
}
|
|
1472
|
+
catch {
|
|
1473
|
+
}
|
|
1474
|
+
return replaceExt(rawName, ext);
|
|
1460
1475
|
}
|
|
1461
1476
|
async _getMessageAudioFileBox(messageId, messagePayload) {
|
|
1462
1477
|
const audioInfo = await parseAudioMessagePayload(messagePayload);
|
package/package.json
CHANGED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env -S node -r ts-node/register
|
|
2
|
+
import { test } from 'tstest'
|
|
3
|
+
|
|
4
|
+
import type { MessagePayload } from '../../engine-schema.js'
|
|
5
|
+
import { parseImageMessagePayload } from './message-image.js'
|
|
6
|
+
|
|
7
|
+
const buildPayload = (imgAttrs: string): MessagePayload => ({
|
|
8
|
+
msg: `<?xml version="1.0"?>\n<msg>\n\t<img ${imgAttrs}>\n\t</img>\n</msg>\n`,
|
|
9
|
+
} as unknown as MessagePayload)
|
|
10
|
+
|
|
11
|
+
test('parseImageMessagePayload uses hevc_mid_size when downloading mid image', async t => {
|
|
12
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
13
|
+
'aeskey="key" cdnmidimgurl="mid-url" length="157626" md5="md5" hevc_mid_size="53274"',
|
|
14
|
+
))
|
|
15
|
+
|
|
16
|
+
t.equal(payload.file_id, 'mid-url', 'should use mid image url')
|
|
17
|
+
t.equal(payload.file_size, '53274', 'size should match the mid image, not the origin length')
|
|
18
|
+
t.equal(payload.is_ww, false, 'should not be ww image')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('parseImageMessagePayload prefers cdnmidimgurl_size', async t => {
|
|
22
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
23
|
+
'aeskey="key" cdnmidimgurl="mid-url" length="157626" hevc_mid_size="53274" cdnmidimgurl_size="60000"',
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
t.equal(payload.file_size, '60000', 'cdnmidimgurl_size should win')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('parseImageMessagePayload falls back to length for legacy xml', async t => {
|
|
30
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
31
|
+
'aeskey="key" cdnmidimgurl="mid-url" length="12345" hevc_mid_size="0"',
|
|
32
|
+
))
|
|
33
|
+
|
|
34
|
+
t.equal(payload.file_size, '12345', 'length should be used when no mid size is available')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('parseImageMessagePayload falls back to length when mid size attrs are absent', async t => {
|
|
38
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
39
|
+
'aeskey="key" cdnmidimgurl="mid-url" length="12345" md5="md5"',
|
|
40
|
+
))
|
|
41
|
+
|
|
42
|
+
t.equal(payload.file_size, '12345', 'length should be used when hevc_mid_size is missing')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('parseImageMessagePayload uses big image url when mid size is missing', async t => {
|
|
46
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
47
|
+
'aeskey="key" cdnmidimgurl="mid-url" cdnbigimgurl="big-url" length="157626"',
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
t.equal(payload.file_id, 'big-url', 'should switch to origin image url')
|
|
51
|
+
t.equal(payload.file_size, '157626', 'size should be origin length')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('parseImageMessagePayload keeps mid url for legacy xml with hdlength', async t => {
|
|
55
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
56
|
+
'aeskey="key" cdnmidimgurl="mid-url" cdnbigimgurl="big-url" length="12345" hdlength="99999"',
|
|
57
|
+
))
|
|
58
|
+
|
|
59
|
+
t.equal(payload.file_id, 'mid-url', 'legacy xml should still use mid url')
|
|
60
|
+
t.equal(payload.file_size, '12345', 'legacy length is mid image size')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('parseImageMessagePayload keeps length for ww image', async t => {
|
|
64
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
65
|
+
'aeskey="key" tpurl="tp-url" tpauthkey="auth" length="888" hevc_mid_size="111"',
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
t.equal(payload.is_ww, true, 'should be ww image')
|
|
69
|
+
t.equal(payload.file_id, 'tp-url', 'should use tpurl')
|
|
70
|
+
t.equal(payload.file_size, '888', 'ww image should keep length')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('parseImageMessagePayload builds fallback candidates for new xml', async t => {
|
|
74
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
75
|
+
'aeskey="key" cdnmidimgurl="mid-url" cdnbigimgurl="big-url" length="157626" hevc_mid_size="53274"',
|
|
76
|
+
))
|
|
77
|
+
|
|
78
|
+
t.same(payload.candidates, [
|
|
79
|
+
{ file_id: 'mid-url', file_size: '53274' },
|
|
80
|
+
{ file_id: 'big-url', file_size: '157626' },
|
|
81
|
+
{ file_id: 'mid-url', file_size: '157626' },
|
|
82
|
+
], 'should try mid size first, then origin, then legacy combination')
|
|
83
|
+
t.equal(payload.file_id, payload.candidates?.[0]?.file_id, 'file_id should be first candidate')
|
|
84
|
+
t.equal(payload.file_size, payload.candidates?.[0]?.file_size, 'file_size should be first candidate')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('parseImageMessagePayload builds fallback candidates for legacy xml', async t => {
|
|
88
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
89
|
+
'aeskey="key" cdnmidimgurl="mid-url" cdnbigimgurl="big-url" length="12345" hdlength="99999"',
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
t.same(payload.candidates, [
|
|
93
|
+
{ file_id: 'mid-url', file_size: '12345' },
|
|
94
|
+
{ file_id: 'big-url', file_size: '99999' },
|
|
95
|
+
], 'legacy xml should try mid + length, then big + hdlength')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('parseImageMessagePayload dedupes candidates', async t => {
|
|
99
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
100
|
+
'aeskey="key" cdnmidimgurl="mid-url" length="157626" hevc_mid_size="53274" cdnmidimgurl_size="53274"',
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
t.same(payload.candidates, [
|
|
104
|
+
{ file_id: 'mid-url', file_size: '53274' },
|
|
105
|
+
{ file_id: 'mid-url', file_size: '157626' },
|
|
106
|
+
], 'same url + size should only appear once')
|
|
107
|
+
})
|
|
@@ -20,6 +20,9 @@ export interface ImageXmlSchema {
|
|
|
20
20
|
length: string
|
|
21
21
|
cdnbigimgurl: string
|
|
22
22
|
hdlength: string
|
|
23
|
+
// 新版 XML 中 length 是原图大小,中图大小单独给出
|
|
24
|
+
hevc_mid_size?: string
|
|
25
|
+
cdnmidimgurl_size?: string
|
|
23
26
|
md5: string
|
|
24
27
|
tpurl?: string
|
|
25
28
|
tpauthkey?: string
|
|
@@ -28,6 +31,11 @@ export interface ImageXmlSchema {
|
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
export interface ImageFileCandidate {
|
|
35
|
+
file_id: string,
|
|
36
|
+
file_size: string,
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
export interface ImageMessagePayload {
|
|
32
40
|
aes_key: string,
|
|
33
41
|
auth_key: string,
|
|
@@ -35,18 +43,71 @@ export interface ImageMessagePayload {
|
|
|
35
43
|
file_size: string,
|
|
36
44
|
file_name: string,
|
|
37
45
|
is_ww: boolean,
|
|
46
|
+
/**
|
|
47
|
+
* 按优先级排列的 (file_id, file_size) 组合,第一项即 file_id/file_size。
|
|
48
|
+
* 下载失败时按顺序回落到下一项,避免在错误的 size 上反复重试。
|
|
49
|
+
*/
|
|
50
|
+
candidates?: ImageFileCandidate[],
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
export async function parseImageMessagePayload (rawPayload: MessagePayload): Promise<ImageMessagePayload> {
|
|
41
54
|
const imageXml: ImageXmlSchema = await xmlToJson(rawPayload.msg)
|
|
42
55
|
const schema = imageXml.msg.img.$
|
|
43
56
|
|
|
57
|
+
const candidates = buildImageCandidates(schema)
|
|
58
|
+
const primary = candidates[0] || { file_id: schema.cdnmidimgurl || schema.tpurl || '', file_size: schema.length }
|
|
59
|
+
|
|
44
60
|
return {
|
|
45
61
|
is_ww: !!schema.tpurl,
|
|
46
62
|
aes_key: schema.aeskey,
|
|
47
63
|
auth_key: schema.tpauthkey || '',
|
|
48
|
-
file_id:
|
|
49
|
-
file_size:
|
|
64
|
+
file_id: primary.file_id,
|
|
65
|
+
file_size: primary.file_size,
|
|
50
66
|
file_name: '',
|
|
67
|
+
candidates: candidates.length ? candidates : [ primary ],
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isPositiveSize = (size?: string): size is string => !!size && Number(size) > 0
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* file_id 和 file_size 必须指向同一张图,否则下载端按错误的 size 做 Range/断点会失败。
|
|
75
|
+
* 候选优先级:
|
|
76
|
+
* 1. 企业微信 (tpurl):保持原逻辑,只有一个候选,size 用 length
|
|
77
|
+
* 2. cdnmidimgurl + 中图大小 (cdnmidimgurl_size,其次 hevc_mid_size)
|
|
78
|
+
* 3. 旧版 XML (有 hdlength,length 是中图大小):cdnmidimgurl + length,其次 cdnbigimgurl + hdlength
|
|
79
|
+
* 新版 XML (无 hdlength,length 是原图大小):cdnbigimgurl + length,其次 cdnmidimgurl + length
|
|
80
|
+
*/
|
|
81
|
+
export function buildImageCandidates (schema: ImageXmlSchema['msg']['img']['$']): ImageFileCandidate[] {
|
|
82
|
+
if (schema.tpurl) {
|
|
83
|
+
return [ { file_id: schema.cdnmidimgurl || schema.tpurl, file_size: schema.length } ]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const midUrl = schema.cdnmidimgurl
|
|
87
|
+
const bigUrl = schema.cdnbigimgurl
|
|
88
|
+
const list: Array<[string | undefined, string | undefined]> = [
|
|
89
|
+
[ midUrl, schema.cdnmidimgurl_size ],
|
|
90
|
+
[ midUrl, schema.hevc_mid_size ],
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
if (isPositiveSize(schema.hdlength)) {
|
|
94
|
+
list.push([ midUrl, schema.length ], [ bigUrl, schema.hdlength ])
|
|
95
|
+
} else {
|
|
96
|
+
list.push([ bigUrl, schema.length ], [ midUrl, schema.length ])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const seen = new Set<string>()
|
|
100
|
+
const candidates: ImageFileCandidate[] = []
|
|
101
|
+
for (const [ fileId, fileSize ] of list) {
|
|
102
|
+
if (!fileId || !isPositiveSize(fileSize)) {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
const key = `${fileId}|${fileSize}`
|
|
106
|
+
if (seen.has(key)) {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
seen.add(key)
|
|
110
|
+
candidates.push({ file_id: fileId, file_size: fileSize })
|
|
51
111
|
}
|
|
112
|
+
return candidates
|
|
52
113
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env -S node -r ts-node/register
|
|
2
|
+
import { test } from 'tstest'
|
|
3
|
+
|
|
4
|
+
import type { MessagePayload } from '../../engine-schema.js'
|
|
5
|
+
import { parseVideoMessagePayload } from './message-video.js'
|
|
6
|
+
import { parseSizeMismatch } from '../service/request.js'
|
|
7
|
+
|
|
8
|
+
const buildPayload = (attrs: string): MessagePayload => ({
|
|
9
|
+
msg: `<?xml version="1.0"?>\n<msg>\n\t<videomsg ${attrs} />\n</msg>\n`,
|
|
10
|
+
} as unknown as MessagePayload)
|
|
11
|
+
|
|
12
|
+
test('parseVideoMessagePayload adds raw video as fallback candidate', async t => {
|
|
13
|
+
const payload = await parseVideoMessagePayload(buildPayload(
|
|
14
|
+
'aeskey="key" cdnvideourl="video-url" length="766838" playlength="5" rawlength="769445" cdnrawvideourl="raw-url" cdnrawvideoaeskey="raw-key"',
|
|
15
|
+
))
|
|
16
|
+
|
|
17
|
+
t.equal(payload.file_id, 'video-url', 'primary file_id unchanged')
|
|
18
|
+
t.equal(payload.file_size, '766838', 'primary file_size unchanged')
|
|
19
|
+
t.same(payload.candidates, [
|
|
20
|
+
{ file_id: 'video-url', file_size: '766838' },
|
|
21
|
+
{ file_id: 'raw-url', file_size: '769445', aes_key: 'raw-key' },
|
|
22
|
+
], 'raw video should be second candidate with its own aes key')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('parseVideoMessagePayload skips empty raw video', async t => {
|
|
26
|
+
const payload = await parseVideoMessagePayload(buildPayload(
|
|
27
|
+
'aeskey="key" cdnvideourl="video-url" length="766838" playlength="5" rawlength="0" cdnrawvideourl="" cdnrawvideoaeskey=""',
|
|
28
|
+
))
|
|
29
|
+
|
|
30
|
+
t.same(payload.candidates, [ { file_id: 'video-url', file_size: '766838' } ], 'only cdn video candidate')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('parseSizeMismatch extracts actual size from backend error', async t => {
|
|
34
|
+
t.equal(parseSizeMismatch('download plaintext size differs: expected 766838, got 1228708'), '1228708', 'should return actual size')
|
|
35
|
+
t.equal(parseSizeMismatch('timeout'), undefined, 'other errors return undefined')
|
|
36
|
+
t.equal(parseSizeMismatch(undefined), undefined, 'undefined returns undefined')
|
|
37
|
+
})
|