import { describe, expect, it, vi } from 'vitest'; import { deflateSync, gunzipSync, gzipSync } from 'node:zlib'; import type { Elem } from '@snowluma/proto-defs/element'; import type { LongMsgResult, RecvLongMsgReq, RecvLongMsgResp, SendLongMsgReq, SendLongMsgResp, } from '@snowluma/proto-defs/longmsg'; import type { PushMsgBody } from '@snowluma/proto-defs/message'; import { buildApiContext, type OneBotInstanceContext } from '@snowluma/onebot/instance-context'; import type { MessageElement } from '@snowluma/protocol/events'; import { protobuf_decode, protobuf_encode } from '@snowluma/proton'; import { ForwardApi } from '../../src/bridge/apis/forward'; import type { BridgeContext } from '../../src/bridge/bridge-context'; import { mockBridge } from './_helpers'; const OUTER_RES_ID = 'outer-resid'; const INNER_UUID = 'C125CE25-AE7E-4E1A-ABDC-9750918162EB'; const INNER_FILE_NAME = `MultiMsg_${INNER_UUID}`; function packetResult(responseData: Uint8Array) { return { success: true, gotResponse: true, errorCode: 0, errorMessage: '', responseData: Buffer.from(responseData), }; } function residLessForwardCard(): Elem { const xml = ``; const data = Buffer.concat([Buffer.from([1]), deflateSync(Buffer.from(xml))]); return { richMsg: { serviceId: 35, template1: data } }; } function message(...elems: Elem[]): PushMsgBody { return { responseHead: { fromUin: 20002, fromUid: 'sender-uid', grp: { groupUin: 12345, memberCard: Buffer.from('alice') }, }, contentHead: { msgType: 82, sequence: 10, timestamp: 1700000000 }, body: { richText: { elems } }, }; } function outerResult(innerActionCommand?: string): LongMsgResult { return { action: [ { actionCommand: 'MultiMsg', actionData: { msgBody: [message(residLessForwardCard())] }, }, ...(innerActionCommand ? [ { actionCommand: innerActionCommand, actionData: { msgBody: [ message({ text: { str: 'inner one' } }), message({ text: { str: 'inner two' } }), ], }, }, ] : []), ], }; } function setup(innerActionCommand?: string) { const requested: string[] = []; const uploads: SendLongMsgReq[] = []; const sendRawPacket = vi.fn(async (command: string, bytes: Uint8Array) => { if (command.endsWith('SsoRecvLongMsg')) { const resId = protobuf_decode(bytes).info!.resId!; requested.push(resId); return packetResult( protobuf_encode({ result: { resId, payload: resId === OUTER_RES_ID ? gzipSync( protobuf_encode( outerResult(innerActionCommand), ), ) : undefined, }, }), ); } expect(command).toMatch(/SsoSendLongMsg$/); const upload = protobuf_decode(bytes); uploads.push(upload); return packetResult( protobuf_encode({ result: { resId: `rebuilt-${uploads.length}` } }), ); }); const bridge = mockBridge({ sendRawPacket }); const forward = new ForwardApi(bridge as unknown as BridgeContext); const sendGroup = vi.fn(async (_groupId: number, _elements: MessageElement[]) => ({ messageId: 1, sequence: 2, clientSequence: 3, random: 4, timestamp: 1700000001, })); const ctx = { selfId: 10001, bridge: { identity: { nickname: 'SnowLuma' }, apis: { forward, message: { sendGroup } }, }, messageStore: { findEvent: () => ({ message: [{ type: 'forward', data: { id: OUTER_RES_ID } }], }), storeEvent: vi.fn(), }, converterCtx: { selfId: 10001, imageUrlResolver: null, mediaUrlResolver: null, messageIdResolver: null, mediaSegmentSink: null, }, cacheMessageMeta: vi.fn(), dispatchEvent: vi.fn(), } as unknown as OneBotInstanceContext; return { api: buildApiContext(ctx), requested, uploads, sendGroup }; } describe('resid-less nested forwards (#589)', () => { it.each([INNER_UUID, INNER_FILE_NAME])( 'resolves an outer piggyback keyed by %s without another download', async (innerActionCommand) => { const { api, requested } = setup(innerActionCommand); const outer = await api.getForwardMsg(OUTER_RES_ID); expect(outer[0]?.message).toEqual([{ type: 'forward', data: { id: INNER_FILE_NAME } }]); const inner = await api.getForwardMsg(INNER_FILE_NAME); expect(inner.map((node) => node.message)).toEqual([ [{ type: 'text', data: { text: 'inner one' } }], [{ type: 'text', data: { text: 'inner two' } }], ]); expect(requested).toEqual([OUTER_RES_ID]); }, ); it('rebuilds both layers before sending to another group', async () => { const { api, requested, uploads, sendGroup } = setup(INNER_UUID); await expect(api.forwardSingleMsg(77, { groupId: 65432 })).resolves.toHaveProperty( 'messageId', ); expect(requested).toEqual([OUTER_RES_ID]); expect(uploads).toHaveLength(2); const outerBytes = gunzipSync(Buffer.from(uploads[1]!.info!.payload!)); const outer = protobuf_decode(outerBytes); expect(outer.action).toHaveLength(2); expect(outerBytes.toString()).not.toContain(INNER_FILE_NAME); expect(sendGroup).toHaveBeenCalledOnce(); expect(sendGroup.mock.calls[0]![1]).toEqual([ expect.objectContaining({ type: 'forward', resId: 'rebuilt-2' }), ]); }); it('fails before upload or send when the inner piggyback is unavailable', async () => { const { api, requested, uploads, sendGroup } = setup(); await expect(api.forwardSingleMsg(77, { groupId: 65432 })).rejects.toThrow( 'download forward message payload is empty', ); expect(requested).toEqual([OUTER_RES_ID, INNER_FILE_NAME]); expect(uploads).toHaveLength(0); expect(sendGroup).not.toHaveBeenCalled(); }); });