wechaty-puppet-matrix 0.0.54 → 0.0.56
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/engine-schema.d.ts +1 -0
- package/dist/cjs/src/engine-schema.d.ts.map +1 -1
- package/dist/cjs/src/matrix/messages/message-contact-card.d.ts +16 -0
- package/dist/cjs/src/matrix/messages/message-contact-card.d.ts.map +1 -0
- package/dist/cjs/src/matrix/messages/message-contact-card.js +25 -0
- package/dist/cjs/src/matrix/messages/message-emotion.d.ts +7 -0
- package/dist/cjs/src/matrix/messages/message-emotion.d.ts.map +1 -1
- package/dist/cjs/src/matrix/messages/message-emotion.js +20 -0
- 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 +21 -0
- package/dist/cjs/src/matrix/service/request.d.ts.map +1 -1
- package/dist/cjs/src/matrix/service/request.js +194 -32
- 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 +3 -0
- package/dist/cjs/src/puppet-matrix.d.ts.map +1 -1
- package/dist/cjs/src/puppet-matrix.js +121 -33
- package/dist/esm/src/engine-schema.d.ts +1 -0
- package/dist/esm/src/engine-schema.d.ts.map +1 -1
- package/dist/esm/src/matrix/messages/message-contact-card.d.ts +16 -0
- package/dist/esm/src/matrix/messages/message-contact-card.d.ts.map +1 -0
- package/dist/esm/src/matrix/messages/message-contact-card.js +22 -0
- package/dist/esm/src/matrix/messages/message-emotion.d.ts +7 -0
- package/dist/esm/src/matrix/messages/message-emotion.d.ts.map +1 -1
- package/dist/esm/src/matrix/messages/message-emotion.js +19 -0
- 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 +21 -0
- package/dist/esm/src/matrix/service/request.d.ts.map +1 -1
- package/dist/esm/src/matrix/service/request.js +193 -32
- 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 +3 -0
- package/dist/esm/src/puppet-matrix.d.ts.map +1 -1
- package/dist/esm/src/puppet-matrix.js +121 -33
- package/package.json +1 -1
- package/src/engine-schema.ts +1 -0
- package/src/matrix/messages/message-contact-card.ts +48 -0
- package/src/matrix/messages/message-emotion.ts +36 -0
- 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 +274 -32
- package/src/matrix/utils/file-type.spec.ts +29 -0
- package/src/matrix/utils/file-type.ts +59 -0
- package/src/puppet-matrix.ts +150 -35
|
@@ -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';
|
|
@@ -16,6 +17,7 @@ import { parseImageMessagePayload } from './matrix/messages/message-image.js';
|
|
|
16
17
|
import { parseAudioMessagePayload } from './matrix/messages/message-audio.js';
|
|
17
18
|
import { parseVideoMessagePayload } from './matrix/messages/message-video.js';
|
|
18
19
|
import { parseLocationMessagePayload } from './matrix/messages/message-location.js';
|
|
20
|
+
import { parseContactCardMessagePayload } from './matrix/messages/message-contact-card.js';
|
|
19
21
|
import { CachedPromiseFunc } from './matrix/utils/cached-promise.js';
|
|
20
22
|
import { engineMessageToWechaty } from './matrix/schema-mapper/message.js';
|
|
21
23
|
import { engineContactToWechaty } from './matrix/schema-mapper/contact.js';
|
|
@@ -23,7 +25,6 @@ import { chatRoomMemberToContact, engineRoomMemberToWechaty, engineRoomToWechaty
|
|
|
23
25
|
import { parseEvent, EventType } from './matrix/events/mod.js';
|
|
24
26
|
import { addRunningPuppet, removeRunningPuppet } from './cleanup.js';
|
|
25
27
|
import { packageJson } from './package-json.js';
|
|
26
|
-
import { JsonToXml } from './matrix/utils/xml-to-json.js';
|
|
27
28
|
const VERSION = packageJson.version || '0.0.0';
|
|
28
29
|
const PRE = '[PuppetMatrix]';
|
|
29
30
|
const SEARCH_CONTACT_PREFIX = '$search$-';
|
|
@@ -520,7 +521,7 @@ class PuppetMatrix extends PUPPET.Puppet {
|
|
|
520
521
|
hello = option.hello;
|
|
521
522
|
}
|
|
522
523
|
}
|
|
523
|
-
await this._client?.addFriendByWxid({ content: hello, ticket, scene: addContactScene, wxid: contactId });
|
|
524
|
+
await this._client?.addFriendByWxid({ content: hello, ticket, scene: addContactScene, wxid: cachedContactSearch?.wxid || contactId });
|
|
524
525
|
}
|
|
525
526
|
async friendshipSearchPhone(phone) {
|
|
526
527
|
return this._friendshipSearch(phone, '15');
|
|
@@ -557,7 +558,58 @@ class PuppetMatrix extends PUPPET.Puppet {
|
|
|
557
558
|
}
|
|
558
559
|
async messageContact(messageId) {
|
|
559
560
|
log.verbose('PuppetWeChat', 'messageContact(%s)', messageId);
|
|
560
|
-
|
|
561
|
+
const rawPayload = await this.messageRawPayload(messageId);
|
|
562
|
+
if (rawPayload.msgType !== WechatMessageType.ShareCard) {
|
|
563
|
+
throw new Error(`message ${messageId} is not a contact card`);
|
|
564
|
+
}
|
|
565
|
+
const card = await parseContactCardMessagePayload(rawPayload.msg);
|
|
566
|
+
if (!card) {
|
|
567
|
+
throw new Error(`can not parse contact card from message ${messageId}`);
|
|
568
|
+
}
|
|
569
|
+
if (!card.username.includes(STRANGER_SUFFIX)) {
|
|
570
|
+
return card.username;
|
|
571
|
+
}
|
|
572
|
+
const friendId = await this._findFriendByCard(card);
|
|
573
|
+
if (friendId) {
|
|
574
|
+
log.verbose(PRE, 'messageContact(%s) matched friend %s', messageId, friendId);
|
|
575
|
+
return friendId;
|
|
576
|
+
}
|
|
577
|
+
const searchId = `${SEARCH_CONTACT_PREFIX}${card.username}`;
|
|
578
|
+
await this._cacheMgr.setContactSearch(searchId, {
|
|
579
|
+
alias: '',
|
|
580
|
+
avatar: card.avatar,
|
|
581
|
+
city: card.city,
|
|
582
|
+
country: '',
|
|
583
|
+
isFriend: 0,
|
|
584
|
+
name: card.nickname,
|
|
585
|
+
province: card.province,
|
|
586
|
+
scene: card.scene,
|
|
587
|
+
sex: card.sex,
|
|
588
|
+
sign: card.sign,
|
|
589
|
+
ticket: card.antispamticket,
|
|
590
|
+
wxNum: card.alias,
|
|
591
|
+
wxid: card.username,
|
|
592
|
+
});
|
|
593
|
+
return searchId;
|
|
594
|
+
}
|
|
595
|
+
async _findFriendByCard(card) {
|
|
596
|
+
const avatarKey = (url) => (url || '').replace(/^https?:\/\//, '').replace(/\/\d+$/, '');
|
|
597
|
+
const cardAvatar = avatarKey(card.avatar);
|
|
598
|
+
const contacts = (await this._cacheMgr.getAllContacts())
|
|
599
|
+
.filter(c => c.wxid && !isRoomId(c.wxid) && !c.wxid.includes(STRANGER_SUFFIX) && c.isFriend !== 0);
|
|
600
|
+
if (cardAvatar) {
|
|
601
|
+
const byAvatar = contacts.find(c => avatarKey(c.avatar) === cardAvatar);
|
|
602
|
+
if (byAvatar) {
|
|
603
|
+
return byAvatar.wxid;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (card.nickname) {
|
|
607
|
+
const byName = contacts.filter(c => c.name === card.nickname);
|
|
608
|
+
if (byName.length === 1) {
|
|
609
|
+
return byName[0].wxid;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return undefined;
|
|
561
613
|
}
|
|
562
614
|
async messageChannel(messageId) {
|
|
563
615
|
log.verbose('PuppetService', 'messageChannel(%s)', messageId);
|
|
@@ -596,12 +648,20 @@ class PuppetMatrix extends PUPPET.Puppet {
|
|
|
596
648
|
return this._getMessageFileFileBox(messageId, messagePayload);
|
|
597
649
|
case PUPPET.types.Message.Emoticon: {
|
|
598
650
|
const emotionPayload = await parseEmotionMessagePayload(messagePayload);
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
651
|
+
const url = emotionPayload.is_ww
|
|
652
|
+
? (await this._client?.downloadWWMedia({
|
|
653
|
+
aes_key: emotionPayload.aeskey || '',
|
|
654
|
+
auth_key: emotionPayload.authkey || '',
|
|
655
|
+
file_id: emotionPayload.cdnurl || '',
|
|
656
|
+
file_name: `message-${messageId}-emoticon.gif`,
|
|
657
|
+
}, 3)) || ''
|
|
658
|
+
: emotionPayload.cdnurl;
|
|
659
|
+
if (!url) {
|
|
660
|
+
throw new Error(`Can not get emoticon url for message: ${messageId}`);
|
|
661
|
+
}
|
|
662
|
+
const head = await fetchRemoteHead(url);
|
|
663
|
+
const ext = (head && detectImageExt(head)) || (emotionPayload.type === 2 && !emotionPayload.game ? 'gif' : 'png');
|
|
664
|
+
const emoticonBox = FileBox.fromUrl(url, { name: `message-${messageId}-emoticon.${ext}` });
|
|
605
665
|
emoticonBox.metadata = {
|
|
606
666
|
payload: emotionPayload,
|
|
607
667
|
type: 'emoticon',
|
|
@@ -736,28 +796,14 @@ class PuppetMatrix extends PUPPET.Puppet {
|
|
|
736
796
|
}
|
|
737
797
|
}
|
|
738
798
|
async messageSendContact(toUserId, contactId) {
|
|
739
|
-
log.verbose('PuppetWeChat', '
|
|
799
|
+
log.verbose('PuppetWeChat', 'messageSendContact("%s", %s)', toUserId, contactId);
|
|
740
800
|
const contactPayload = await this.contactRawPayload(contactId);
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
city: contactPayload?.city,
|
|
748
|
-
fullpy: contactPayload?.name,
|
|
749
|
-
imagestatus: '3',
|
|
750
|
-
nickname: contactPayload?.name,
|
|
751
|
-
province: contactPayload?.province,
|
|
752
|
-
regionCode: 'CN_Shanghai',
|
|
753
|
-
scene: '17',
|
|
754
|
-
sex: contactPayload?.sex,
|
|
755
|
-
smallheadimgurl: contactPayload?.avatar,
|
|
756
|
-
username: contactPayload?.ticket,
|
|
757
|
-
},
|
|
758
|
-
};
|
|
759
|
-
const xml = JsonToXml(xmlObj);
|
|
760
|
-
const res = await this._client?.sendContactCard(toUserId, xml, this._self?.wxid);
|
|
801
|
+
const res = await this._client?.sendContact({
|
|
802
|
+
shareHeadImg: contactPayload?.avatar,
|
|
803
|
+
shareId: contactPayload?.wxid || contactId,
|
|
804
|
+
shareNickname: contactPayload?.name,
|
|
805
|
+
toId: toUserId,
|
|
806
|
+
}, this._self?.wxid);
|
|
761
807
|
if (res) {
|
|
762
808
|
await this._cacheMgr?.setMessage(res.id, res);
|
|
763
809
|
return res.id;
|
|
@@ -770,7 +816,16 @@ class PuppetMatrix extends PUPPET.Puppet {
|
|
|
770
816
|
: path.extname(fileBox.name);
|
|
771
817
|
const fileUrl = fileBox.remoteUrl || '';
|
|
772
818
|
if (metadata.type === 'emoticon') {
|
|
773
|
-
|
|
819
|
+
const emoji = metadata.payload;
|
|
820
|
+
const byMd5 = emoji?.md5 && emoji.len
|
|
821
|
+
? await this._client?.sendEmojiMd5(conversationId, emoji.md5, emoji.len, this._self?.wxid)
|
|
822
|
+
: undefined;
|
|
823
|
+
const msgInfo = byMd5 || (fileUrl ? await this._client?.sendEmojiUrl(conversationId, fileUrl, this._self?.wxid) : undefined);
|
|
824
|
+
if (!msgInfo) {
|
|
825
|
+
throw new Error(`send emoticon failed: ${fileBox.name}`);
|
|
826
|
+
}
|
|
827
|
+
await this._cacheMgr?.setMessage(msgInfo.id, msgInfo);
|
|
828
|
+
return msgInfo.id;
|
|
774
829
|
}
|
|
775
830
|
else if (fileBox.mediaType.startsWith('image/')) {
|
|
776
831
|
if (fileUrl) {
|
|
@@ -875,8 +930,27 @@ class PuppetMatrix extends PUPPET.Puppet {
|
|
|
875
930
|
throw new Error(`Wrong Post!!! please check your Post payload to make sure it right ${conversationId}`);
|
|
876
931
|
}
|
|
877
932
|
}
|
|
933
|
+
async sendPat(conversationId, contactId) {
|
|
934
|
+
log.verbose(PRE, 'sendPat(%s, %s)', conversationId, contactId);
|
|
935
|
+
const patted = contactId || conversationId;
|
|
936
|
+
if (isRoomId(patted)) {
|
|
937
|
+
throw new Error('sendPat: contactId is required in room');
|
|
938
|
+
}
|
|
939
|
+
return (await this._client?.sendPat(conversationId, patted)) || false;
|
|
940
|
+
}
|
|
878
941
|
async messageRecall(messageId) {
|
|
879
|
-
|
|
942
|
+
log.verbose(PRE, 'messageRecall(%s)', messageId);
|
|
943
|
+
const payload = await this._cacheMgr?.getMessage(messageId);
|
|
944
|
+
if (!payload) {
|
|
945
|
+
log.warn(PRE, 'messageRecall(%s): message not found in cache', messageId);
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
const toId = payload.listenerId || payload.fromWxid || '';
|
|
949
|
+
if (!toId) {
|
|
950
|
+
log.warn(PRE, 'messageRecall(%s): can not find conversation id', messageId);
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
return (await this._client?.revokeMsg(toId, messageId, payload.clientMsgId)) || false;
|
|
880
954
|
}
|
|
881
955
|
async messageForward(toUserName, messageId) {
|
|
882
956
|
const messagePayload = await this.messageRawPayload(messageId);
|
|
@@ -1456,7 +1530,21 @@ class PuppetMatrix extends PUPPET.Puppet {
|
|
|
1456
1530
|
log.error(`Can not get file for message: ${messageId}`);
|
|
1457
1531
|
}
|
|
1458
1532
|
const url = imageInfo.is_ww ? await this._client?.downloadWWMedia(imageInfo, 1) : await this._client?.downloadImage(imageInfo);
|
|
1459
|
-
return this._fileBoxFromUrl(url, messageId);
|
|
1533
|
+
return this._fileBoxFromUrl(url, messageId, url ? await this._imageNameByHead(url) : undefined);
|
|
1534
|
+
}
|
|
1535
|
+
async _imageNameByHead(url) {
|
|
1536
|
+
const head = await fetchRemoteHead(url);
|
|
1537
|
+
const ext = head && detectImageExt(head);
|
|
1538
|
+
if (!ext) {
|
|
1539
|
+
return undefined;
|
|
1540
|
+
}
|
|
1541
|
+
let rawName = 'image';
|
|
1542
|
+
try {
|
|
1543
|
+
rawName = decodeURIComponent(new URL(url).pathname.split('/').pop() || rawName);
|
|
1544
|
+
}
|
|
1545
|
+
catch {
|
|
1546
|
+
}
|
|
1547
|
+
return replaceExt(rawName, ext);
|
|
1460
1548
|
}
|
|
1461
1549
|
async _getMessageAudioFileBox(messageId, messagePayload) {
|
|
1462
1550
|
const audioInfo = await parseAudioMessagePayload(messagePayload);
|
package/package.json
CHANGED
package/src/engine-schema.ts
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { xmlToJson } from '../utils/xml-to-json.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 名片消息(msgType 42)
|
|
5
|
+
*
|
|
6
|
+
* <msg username="wxid_xxx | v3_xxx@stranger" nickname="" alias="" bigheadimgurl="" smallheadimgurl=""
|
|
7
|
+
* province="" city="" sex="1" sign="" scene="17" antispamticket="v4_xxx@stranger" ... />
|
|
8
|
+
*/
|
|
9
|
+
export interface ContactCardPayload {
|
|
10
|
+
/** 好友为 wxid,非好友为 v3_xxx@stranger 加密用户名 */
|
|
11
|
+
username: string
|
|
12
|
+
nickname: string
|
|
13
|
+
/** 微信号 */
|
|
14
|
+
alias: string
|
|
15
|
+
avatar: string
|
|
16
|
+
province: string
|
|
17
|
+
city: string
|
|
18
|
+
sex: number
|
|
19
|
+
sign: string
|
|
20
|
+
scene: string
|
|
21
|
+
/** 添加好友用的 v4 票据 */
|
|
22
|
+
antispamticket: string
|
|
23
|
+
/** 公众号名片 brandType/certflag 非 0 */
|
|
24
|
+
brandType: number
|
|
25
|
+
certflag: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function parseContactCardMessagePayload (content: string): Promise<ContactCardPayload | undefined> {
|
|
29
|
+
const json = await xmlToJson(content)
|
|
30
|
+
const attrs = json?.msg?.$
|
|
31
|
+
if (!attrs?.username) {
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
alias: attrs.alias || '',
|
|
36
|
+
antispamticket: attrs.antispamticket || '',
|
|
37
|
+
avatar: attrs.bigheadimgurl || attrs.smallheadimgurl || '',
|
|
38
|
+
brandType: parseInt(attrs.brandType, 10) || 0,
|
|
39
|
+
certflag: parseInt(attrs.certflag, 10) || 0,
|
|
40
|
+
city: attrs.city || '',
|
|
41
|
+
nickname: attrs.nickname || '',
|
|
42
|
+
province: attrs.province || '',
|
|
43
|
+
scene: attrs.scene || '17',
|
|
44
|
+
sex: parseInt(attrs.sex, 10) || 0,
|
|
45
|
+
sign: attrs.sign || '',
|
|
46
|
+
username: attrs.username,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -26,6 +26,37 @@ interface EmotionXmlSchema {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* 表情小游戏结果
|
|
31
|
+
* gameext type=1 猜拳: content 1=剪刀 2=石头 3=布
|
|
32
|
+
* gameext type=2 骰子: content 4~9 对应 1~6 点
|
|
33
|
+
*/
|
|
34
|
+
export interface EmojiGamePayload {
|
|
35
|
+
kind: 'rps' | 'dice';
|
|
36
|
+
/** 猜拳: scissors | rock | paper;骰子: 1~6 */
|
|
37
|
+
value: string | number;
|
|
38
|
+
/** 中文描述,如 "石头"、"6点" */
|
|
39
|
+
text: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const RPS: Record<number, [string, string]> = {
|
|
43
|
+
1: [ 'scissors', '剪刀' ],
|
|
44
|
+
2: [ 'rock', '石头' ],
|
|
45
|
+
3: [ 'paper', '布' ],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseEmojiGame (type: number, content: number): EmojiGamePayload | undefined {
|
|
49
|
+
if (type === 1 && RPS[content]) {
|
|
50
|
+
const [ value, text ] = RPS[content]!
|
|
51
|
+
return { kind: 'rps', text, value }
|
|
52
|
+
}
|
|
53
|
+
if (type === 2 && content >= 4 && content <= 9) {
|
|
54
|
+
const point = content - 3
|
|
55
|
+
return { kind: 'dice', text: `${point}点`, value: point }
|
|
56
|
+
}
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
29
60
|
export interface EmojiMessagePayload {
|
|
30
61
|
type: number;
|
|
31
62
|
len: number;
|
|
@@ -34,6 +65,8 @@ export interface EmojiMessagePayload {
|
|
|
34
65
|
width: number;
|
|
35
66
|
height: number;
|
|
36
67
|
gameext?: string;
|
|
68
|
+
/** 骰子 / 猜拳的可读结果,普通表情为 undefined */
|
|
69
|
+
game?: EmojiGamePayload;
|
|
37
70
|
aeskey?: string;
|
|
38
71
|
authkey?: string;
|
|
39
72
|
is_ww: boolean;
|
|
@@ -50,10 +83,12 @@ export async function parseEmotionMessagePayload (message: MessagePayload): Prom
|
|
|
50
83
|
const md5 = jsonPayload.msg.emoji.$.md5
|
|
51
84
|
|
|
52
85
|
let gameext
|
|
86
|
+
let game: EmojiGamePayload | undefined
|
|
53
87
|
if (jsonPayload.msg.gameext) {
|
|
54
88
|
const gameextType = parseInt(jsonPayload.msg.gameext.$.type, 10) || 0
|
|
55
89
|
const gameextContent = parseInt(jsonPayload.msg.gameext.$.content, 10) || 0
|
|
56
90
|
gameext = `<gameext type="${gameextType}" content="${gameextContent}" ></gameext>`
|
|
91
|
+
game = parseEmojiGame(gameextType, gameextContent)
|
|
57
92
|
}
|
|
58
93
|
|
|
59
94
|
return {
|
|
@@ -61,6 +96,7 @@ export async function parseEmotionMessagePayload (message: MessagePayload): Prom
|
|
|
61
96
|
aeskey: jsonPayload.msg.emoji.$?.aeskey,
|
|
62
97
|
is_ww: !!jsonPayload.msg.emoji.$?.tpurl,
|
|
63
98
|
cdnurl,
|
|
99
|
+
game,
|
|
64
100
|
gameext,
|
|
65
101
|
height,
|
|
66
102
|
len,
|
|
@@ -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
|
+
})
|
|
@@ -50,9 +50,17 @@ export interface VideoMessagePayload {
|
|
|
50
50
|
thumb_size: string,
|
|
51
51
|
thumb_width: string,
|
|
52
52
|
thumb_height: string,
|
|
53
|
-
file_name: ''
|
|
53
|
+
file_name: '',
|
|
54
|
+
/**
|
|
55
|
+
* 按优先级排列的下载候选,第一项即 file_id/file_size。
|
|
56
|
+
* 实测 XML 的 length 可能和 CDN 实际大小不一致 (expected 766838, got 1228708),
|
|
57
|
+
* 有原始视频时回落到 cdnrawvideourl + rawlength
|
|
58
|
+
*/
|
|
59
|
+
candidates?: Array<{ file_id: string, file_size: string, aes_key?: string }>,
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
const isPositiveSize = (size?: string): size is string => !!size && Number(size) > 0
|
|
63
|
+
|
|
56
64
|
export async function parseVideoMessagePayload (rawPayload: MessagePayload): Promise<VideoMessagePayload> {
|
|
57
65
|
const videoXml: VideoXmlSchema = await xmlToJson(rawPayload.msg)
|
|
58
66
|
const videoSchema = videoXml.msg.videomsg.$
|
|
@@ -70,5 +78,24 @@ export async function parseVideoMessagePayload (rawPayload: MessagePayload): Pro
|
|
|
70
78
|
thumb_width: videoSchema.cdnthumbwidth || videoSchema.tpthumbwidth || '',
|
|
71
79
|
thumb_height: videoSchema.cdnthumbheight || videoSchema.tpthumbheight || '',
|
|
72
80
|
file_name: '',
|
|
81
|
+
candidates: buildVideoCandidates(videoSchema),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildVideoCandidates (schema: VideoXmlSchema['msg']['videomsg']['$']): VideoMessagePayload['candidates'] {
|
|
86
|
+
if (schema.tpvideourl) {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
const candidates: NonNullable<VideoMessagePayload['candidates']> = []
|
|
90
|
+
if (schema.cdnvideourl && isPositiveSize(schema.length)) {
|
|
91
|
+
candidates.push({ file_id: schema.cdnvideourl, file_size: schema.length })
|
|
92
|
+
}
|
|
93
|
+
if (schema.cdnrawvideourl && isPositiveSize(schema.rawlength)) {
|
|
94
|
+
candidates.push({
|
|
95
|
+
file_id: schema.cdnrawvideourl,
|
|
96
|
+
file_size: schema.rawlength,
|
|
97
|
+
aes_key: schema.cdnrawvideoaeskey || schema.aeskey,
|
|
98
|
+
})
|
|
73
99
|
}
|
|
100
|
+
return candidates.length ? candidates : undefined
|
|
74
101
|
}
|