Files
SnowLuma/packages/protocol/tests/msg-push/empty-message-drop.test.ts
T
DrSmoothl e0d5b47488 feat(msg-push): filter C2C control pushes by c2c_cmd, matching QQ NT (#102)
Upgrades the blank-body heuristic to NT's exact discriminator. RE of
wrapper.linux.node (msg_header_codec_helper.cc::DecodeRoutingHead, sub_3B42650)
shows the official client classifies a C2C push by (svr_msg_type, c2c_cmd):
msgType ∈ {141,166,167} with c2c_cmd ∈ {1,73,75,129,131,133,135,192} is routed
through OnRecvSysMsg (long_cnn_msg_mgr.cc) as a system/control signal and never
shown as a chat bubble — the group-invite "[空消息]" phantom is one of these.

- proto-defs: ContentHead field 3 was mislabelled `divSeq`; it is `c2c_cmd`.
  Renamed (only other use was forward.ts, which zeroes it on a fake send).
- MsgPushHead now carries c2cCmd; new isC2cControlPush() in blank-filter.ts.
- parseMsgPush + both history fetches drop control pushes by (msgType, c2cCmd)
  regardless of body, so a control signal carrying stray content is caught too
  (not just the empty case). isBlankMessage stays as the fallback for
  content-less pushes we don't yet classify by cmd. Live path debug-logs a
  control push that carried elements, in case the table ever eats real content.

NOTE: the c2c_cmd set is taken from NT; we have not captured #24's actual cmd
(our diagnostic never logged field 3), so the blank fallback remains load-bearing
until a live capture confirms which cmd the invite phantom uses.
2026-06-16 01:34:38 +08:00

83 lines
3.8 KiB
TypeScript

// Regression for #102: a group invite makes QQ emit a content-less c2c push
// (msgType=166/0, body.richText.elems=[]) right after the invite card. That
// phantom used to surface to OneBot clients as a confusing "[空消息]".
// parseMsgPush must drop a message that decodes to zero elements *when its body
// is genuinely empty*, while still keeping (and warning about) a message that
// carried content we merely failed to decode.
import { describe, expect, it } from 'vitest';
import type { PacketInfo } from '@snowluma/common/protocol-types';
import { protobuf_encode } from '@snowluma/proton';
import type { PushMsg, PushMsgBody } from '@snowluma/proto-defs/message';
import type { IdentityService } from '../../src/identity-service';
import { parseMsgPush } from '../../src/msg-push';
const identity = { findFriend: () => undefined } as unknown as IdentityService;
function pushPacket(message: PushMsgBody): PacketInfo {
return {
pid: 0,
uin: '2000000001',
serviceCmd: 'trpc.msg.olpush.OlPushService.MsgPush',
seqId: 0,
retCode: 0,
fromClient: false,
body: protobuf_encode<PushMsg>({ message }),
};
}
describe('parseMsgPush — empty message drop (#102)', () => {
it('drops a genuinely empty friend message (group-invite phantom)', () => {
const out = parseMsgPush(pushPacket({
responseHead: { fromUin: 10001, fromUid: 'u_x' },
contentHead: { msgType: 166, subType: 0, sequence: 59962, timestamp: 1781540572, msgId: 1963990184 },
body: { richText: { elems: [] } },
}), identity);
expect(out).toEqual([]);
});
it('keeps a friend message that actually has content', () => {
const out = parseMsgPush(pushPacket({
responseHead: { fromUin: 10001, fromUid: 'u_x' },
contentHead: { msgType: 166, subType: 0, sequence: 100, timestamp: 1781540572, msgId: 1 },
body: { richText: { elems: [{ text: { str: 'hi' } }] } },
}), identity);
expect(out).toHaveLength(1);
expect(out[0]).toMatchObject({ kind: 'friend_message', elements: [{ type: 'text', text: 'hi' }] });
});
it('drops a C2C control push by (msgType, c2cCmd) even when it carries content', () => {
// 166 + c2c_cmd ∈ {1,73,75,129,131,133,135,192} is a control/system signal
// QQ NT routes via OnRecvSysMsg, never a bubble — drop regardless of body.
const out = parseMsgPush(pushPacket({
responseHead: { fromUin: 10001, fromUid: 'u_x' },
contentHead: { msgType: 166, subType: 0, c2cCmd: 75, sequence: 200, timestamp: 1781540572, msgId: 3 },
body: { richText: { elems: [{ text: { str: 'noise' } }] } },
}), identity);
expect(out).toEqual([]);
});
it('keeps a normal 166 message whose c2cCmd is not a control command', () => {
const out = parseMsgPush(pushPacket({
responseHead: { fromUin: 10001, fromUid: 'u_x' },
contentHead: { msgType: 166, subType: 0, c2cCmd: 0, sequence: 201, timestamp: 1781540572, msgId: 4 },
body: { richText: { elems: [{ text: { str: 'hi' } }] } },
}), identity);
expect(out).toHaveLength(1);
expect(out[0]).toMatchObject({ kind: 'friend_message', elements: [{ type: 'text', text: 'hi' }] });
});
it('keeps an empty-decoded message whose body still carried content (missing decoder)', () => {
// A commonElem service type the rich-body decoder ignores → 0 elements, but
// the body was not empty, so the message must survive (and be warned about),
// not be silently dropped.
const out = parseMsgPush(pushPacket({
responseHead: { fromUin: 10001, fromUid: 'u_x' },
contentHead: { msgType: 166, subType: 0, sequence: 101, timestamp: 1781540572, msgId: 2 },
body: { richText: { elems: [{ commonElem: { serviceType: 999, businessType: 0 } }] } },
}), identity);
expect(out).toHaveLength(1);
expect(out[0]).toMatchObject({ kind: 'friend_message', elements: [] });
});
});