fix(send): use c2c businessType for private video / record
Private-chat video and voice sends were getting bounced by the QQ NT server with `PbSendMsg result=79`. Highway upload logs said success, but the final SendMsg the bot constructed used the group-scene tag inside the outgoing commonElem, and the c2c routing path doesn't recognise it. The convention — verified against the receive-side decoder at msg-push/rich-body-decoder.ts:261-330 which explicitly branches on the pairs — is: image : commonElem.businessType c2c=10 group=20 video : commonElem.businessType c2c=11 group=21 record : commonElem.businessType c2c=12 group=22 makeImageElem already honoured the split (`isGroup ? 20 : 10`). makeVideoElem and makePttElem hardcoded the group value for both scenes. Fix them to follow the same pattern. Also fix a symmetric asymmetry inside the highway upload itself: uploadVideoMsgInfo hardcoded `compatQmsgSceneType: 2` while uploadImageMsgInfo and uploadPttMsgInfo both use `isGroup ? 2 : 1`. Same root cause — group value leaking into the c2c path. Regression test (tests/element-builder.test.ts) pins all six businessType pairs by mocking the highway upload functions and inspecting the proto element the builder returns; would have caught the bug had it existed at the time element-builder.ts was written.
This commit is contained in:
@@ -208,11 +208,14 @@ async function makePttElem(ctx: SendContext, element: MessageElement): Promise<P
|
||||
|
||||
const msgInfo = await uploadPttMsgInfo(ctx.bridge, isGroup, targetIdOrUid, element);
|
||||
|
||||
// commonElem.businessType is the QQ NT scene tag the receive-side
|
||||
// decoder pairs with: 12=c2c, 22=group. Sending the group tag on a
|
||||
// c2c message bounces with PbSendMsg result=79.
|
||||
return {
|
||||
commonElem: {
|
||||
serviceType: 48,
|
||||
pbElem: msgInfo,
|
||||
businessType: 22,
|
||||
businessType: isGroup ? 22 : 12,
|
||||
} as any,
|
||||
};
|
||||
}
|
||||
@@ -226,11 +229,14 @@ async function makeVideoElem(ctx: SendContext, element: MessageElement): Promise
|
||||
|
||||
const msgInfo = await uploadVideoMsgInfo(ctx.bridge, isGroup, targetIdOrUid, element);
|
||||
|
||||
// commonElem.businessType is the QQ NT scene tag the receive-side
|
||||
// decoder pairs with: 11=c2c, 21=group. Sending the group tag on a
|
||||
// c2c message bounces with PbSendMsg result=79.
|
||||
return {
|
||||
commonElem: {
|
||||
serviceType: 48,
|
||||
pbElem: msgInfo,
|
||||
businessType: 21,
|
||||
businessType: isGroup ? 21 : 11,
|
||||
} as any,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -487,7 +487,11 @@ export async function uploadVideoMsgInfo(
|
||||
subFileType: 100,
|
||||
},
|
||||
],
|
||||
compatQmsgSceneType: 2,
|
||||
// Scene tag the NTV2 upload registers under. image-upload and
|
||||
// ptt-upload both differentiate (group=2, c2c=1); video was
|
||||
// hardcoded to the group value, which contributed to the c2c
|
||||
// send-back rejection (PbSendMsg result=79).
|
||||
compatQmsgSceneType: isGroup ? 2 : 1,
|
||||
extBizInfo: {
|
||||
pic: { bizType: 0, textSummary: 'Nya~' },
|
||||
video: { bytesPbReserve: new Uint8Array([0x80, 0x01, 0x00]) },
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// Regression test for the c2c-vs-group businessType asymmetry that made
|
||||
// private-chat video / record sends bounce with `send private message
|
||||
// rejected: result=79`. The receive-side decoder
|
||||
// (msg-push/rich-body-decoder.ts) explicitly treats the businessType
|
||||
// pairs as:
|
||||
//
|
||||
// image : c2c=10, group=20
|
||||
// video : c2c=11, group=21
|
||||
// record : c2c=12, group=22
|
||||
//
|
||||
// `makeImageElem` always honoured this split (`isGroup ? 20 : 10`).
|
||||
// `makeVideoElem` and `makePttElem` used to hardcode the group value
|
||||
// for both scenes, so any c2c video / private voice send arrived at the
|
||||
// QQ NT server with a businessType the c2c routing path did not
|
||||
// recognise and got rejected with result=79.
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('../src/bridge/highway/image-upload', () => ({
|
||||
uploadImageMsgInfo: vi.fn(async () => new Uint8Array([7, 8, 9])),
|
||||
}));
|
||||
vi.mock('../src/bridge/highway/ptt-upload', () => ({
|
||||
uploadPttMsgInfo: vi.fn(async () => new Uint8Array([4, 5, 6])),
|
||||
}));
|
||||
vi.mock('../src/bridge/highway/video-upload', () => ({
|
||||
uploadVideoMsgInfo: vi.fn(async () => new Uint8Array([1, 2, 3])),
|
||||
}));
|
||||
|
||||
import { buildSendElems } from '../src/bridge/element-builder';
|
||||
|
||||
const fakeBridge = {} as any;
|
||||
|
||||
function commonElem(elem: any): { serviceType: number; businessType: number; pbElem: Uint8Array } {
|
||||
return elem.commonElem;
|
||||
}
|
||||
|
||||
describe('element-builder / commonElem.businessType per scene', () => {
|
||||
describe('image', () => {
|
||||
it('c2c → businessType 10', async () => {
|
||||
const [elem] = await buildSendElems(
|
||||
[{ type: 'image', url: 'file:///tmp/a.png' } as any],
|
||||
{ bridge: fakeBridge, userUid: 'u_peer' },
|
||||
);
|
||||
expect(commonElem(elem).serviceType).toBe(48);
|
||||
expect(commonElem(elem).businessType).toBe(10);
|
||||
});
|
||||
|
||||
it('group → businessType 20', async () => {
|
||||
const [elem] = await buildSendElems(
|
||||
[{ type: 'image', url: 'file:///tmp/a.png' } as any],
|
||||
{ bridge: fakeBridge, groupId: 12345 },
|
||||
);
|
||||
expect(commonElem(elem).businessType).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('video', () => {
|
||||
it('c2c → businessType 11 (regression: was 21, server returned result=79)', async () => {
|
||||
const [elem] = await buildSendElems(
|
||||
[{ type: 'video', url: 'file:///tmp/clip.mp4' } as any],
|
||||
{ bridge: fakeBridge, userUid: 'u_peer' },
|
||||
);
|
||||
expect(commonElem(elem).serviceType).toBe(48);
|
||||
expect(commonElem(elem).businessType).toBe(11);
|
||||
});
|
||||
|
||||
it('group → businessType 21', async () => {
|
||||
const [elem] = await buildSendElems(
|
||||
[{ type: 'video', url: 'file:///tmp/clip.mp4' } as any],
|
||||
{ bridge: fakeBridge, groupId: 12345 },
|
||||
);
|
||||
expect(commonElem(elem).businessType).toBe(21);
|
||||
});
|
||||
});
|
||||
|
||||
describe('record', () => {
|
||||
it('c2c → businessType 12 (regression: was 22)', async () => {
|
||||
const [elem] = await buildSendElems(
|
||||
[{ type: 'record', url: 'file:///tmp/voice.amr' } as any],
|
||||
{ bridge: fakeBridge, userUid: 'u_peer' },
|
||||
);
|
||||
expect(commonElem(elem).serviceType).toBe(48);
|
||||
expect(commonElem(elem).businessType).toBe(12);
|
||||
});
|
||||
|
||||
it('group → businessType 22', async () => {
|
||||
const [elem] = await buildSendElems(
|
||||
[{ type: 'record', url: 'file:///tmp/voice.amr' } as any],
|
||||
{ bridge: fakeBridge, groupId: 12345 },
|
||||
);
|
||||
expect(commonElem(elem).businessType).toBe(22);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user