Files
WechatExplorer/tests/unit/message-parser.test.ts
T

178 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import { parseMessageContent } from '../../src/main/message-parser'
describe('message parser', () => {
it('parses image, voice and sticker messages without confusing their types', () => {
expect(parseMessageContent('<img md5="0123456789abcdef0123456789abcdef" />', 3)).toMatchObject({
type: 'image',
md5: '0123456789abcdef0123456789abcdef'
})
expect(parseMessageContent('voice fixture', 34)).toEqual({ type: 'voice' })
expect(parseMessageContent('', 34)).toEqual({ type: 'voice' })
expect(
parseMessageContent(
'<emoji md5="abcdefabcdefabcdefabcdefabcdefab" cdnurl="https://fixture.invalid/a" />',
47
)
).toMatchObject({ type: 'sticker', md5: 'abcdefabcdefabcdefabcdefabcdefab' })
})
it('keeps video metadata when WeChat omits every MD5 field', () => {
const parsed = parseMessageContent(
'<msg><videomsg length="6402169" playlength="30" cdnthumbwidth="224" cdnthumbheight="398" aeskey="25201cc658042689d1ad6747cea2b240" rawmd5="" /></msg>',
43
)
expect(parsed).toEqual({
type: 'video',
md5: undefined,
newMd5: undefined,
rawMd5: undefined,
byteLength: 6402169,
duration: 30,
width: 224,
height: 398
})
})
it('parses merged forwards and preserves nested visible text', () => {
const parsed = parseMessageContent(
'<appmsg><type>19</type><title>转发多条内容</title><recorditem><dataitem datatype="1"><sourcename>测试成员</sourcename><datadesc>脱敏内容</datadesc></dataitem></recorditem></appmsg>',
49
)
expect(parsed.type).toBe('forwardBundle')
if (parsed.type === 'forwardBundle') {
expect(parsed.title).toBe('转发多条内容')
expect(parsed.items.map((item) => item.text).join(' ')).toContain('脱敏内容')
}
})
it.each([
['6', '测试附件.pdf'],
['74', '发送中的附件.zip']
])(
'keeps file app message type %s when attachment metadata contains record tags',
(typeVal, title) => {
const parsed = parseMessageContent(
`<appmsg><type>${typeVal}</type><title>${title}</title><des>1 MB</des><appattach><recorditem>legacy metadata</recorditem><dataitem datatype="8"><datatitle>${title}</datatitle></dataitem></appattach></appmsg>`,
49
)
expect(parsed).toMatchObject({
type: 'share',
title,
typeVal
})
}
)
it('decodes XML entities in file titles used for attachment lookup', () => {
const parsed = parseMessageContent(
'<appmsg><type>6</type><title>Check-in Voucher (Samabe Bali Suites &amp; Villas).pdf</title></appmsg>',
49
)
expect(parsed).toMatchObject({
type: 'share',
title: 'Check-in Voucher (Samabe Bali Suites & Villas).pdf',
typeVal: '6'
})
})
it('does not classify empty incidental record metadata as a merged forward', () => {
const parsed = parseMessageContent(
'<appmsg><type>5</type><title>普通分享</title><recorditem>legacy metadata</recorditem></appmsg>',
49
)
expect(parsed).toMatchObject({ type: 'share', title: '普通分享', typeVal: '5' })
})
it('preserves every article in a public-account multi-article message', () => {
const parsed = parseMessageContent(
`<appmsg><type>5</type><appname>长江日报</appname><mmreader><category count="3"><item><title><![CDATA[女子吃酒席时意外发现]]></title><url><![CDATA[https://mp.weixin.qq.com/a]]></url><cover><![CDATA[https://img.test/a.jpg]]></cover></item><item><title>霍尔木兹海峡开放临时协议</title><digest>国际油价短期走势</digest><url>https://mp.weixin.qq.com/b</url></item><item><title>东野圭吾新作</title><url>https://mp.weixin.qq.com/c</url></item></category></mmreader></appmsg>`,
49
)
expect(parsed).toMatchObject({ type: 'share', appname: '长江日报' })
if (parsed.type === 'share') {
expect(parsed.articles).toHaveLength(3)
expect(parsed.articles?.map((article) => article.title)).toEqual([
'女子吃酒席时意外发现',
'霍尔木兹海峡开放临时协议',
'东野圭吾新作'
])
}
})
it('uses the quoted group member id instead of the chatroom id', () => {
const parsed = parseMessageContent(
'<appmsg><type>57</type><title>回复内容</title><refermsg><type>1</type><fromusr>123456789@chatroom</fromusr><chatusr>wxid_fixture_member</chatusr><content>被引用内容</content></refermsg></appmsg>',
49
)
expect(parsed).toMatchObject({
type: 'quote',
quotedSender: 'wxid_fixture_member',
quotedContent: '被引用内容'
})
})
it('renders the templated join-group notice instead of its hidden button label', () => {
// 微信 4.x 的 sysmsgtemplate:<plain> 为空、正文在 <template> 里用 $名称$ 引用 link,
// hidden="1" 的 link 是可点击按钮,不应作为正文。
const parsed = parseMessageContent(
[
'<sysmsg type="sysmsgtemplate">',
'<sysmsgtemplate><content_template type="tmpl_type_profilewithrevokeqrcode">',
'<plain><![CDATA[]]></plain>',
'<template><![CDATA["$adder$"通过扫描你分享的二维码加入群聊 $revoke$]]></template>',
'<link_list>',
'<link name="adder" type="link_profile"><memberlist><member>',
'<username><![CDATA[wxid_fixture_member]]></username>',
'<nickname><![CDATA[成员昵称]]></nickname>',
'</member></memberlist></link>',
'<link name="revoke" type="link_revoke_qrcode" hidden="1">',
'<title><![CDATA[撤销]]></title>',
'</link>',
'</link_list>',
'</content_template></sysmsgtemplate></sysmsg>'
].join(''),
10000
)
expect(parsed).toMatchObject({
type: 'system',
content: '"成员昵称"通过扫描你分享的二维码加入群聊'
})
})
it('keeps parsing the legacy delchatroommember join-group notice', () => {
const parsed = parseMessageContent(
[
'<sysmsg type="delchatroommember"><delchatroommember>',
'<plain><![CDATA["成员昵称"通过扫描你分享的二维码加入群聊 ]]></plain>',
'<text><![CDATA["成员昵称"通过扫描你分享的二维码加入群聊 ]]></text>',
'<link><scene>qrcode</scene><text><![CDATA[ 撤销]]></text>',
'<memberlist><username><![CDATA[wxid_fixture_member]]></username></memberlist>',
'</link>',
'</delchatroommember></sysmsg>'
].join(''),
10000
)
expect(parsed).toMatchObject({
type: 'system',
content: '"成员昵称"通过扫描你分享的二维码加入群聊'
})
})
it('uses an explicit unknown type for unsupported messages', () => {
expect(parseMessageContent('opaque fixture payload', 999)).toEqual({
type: 'unknown',
raw: 'opaque fixture payload',
messageType: 999
})
})
})