fix(protocol): restore unidirectional friend lookup
Read the unidirectional friend list correctly and preserve service errors instead of returning an empty list for rejected requests. Fixes #608
This commit is contained in:
@@ -352,12 +352,12 @@ describe('apis/profile', () => {
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.from(
|
||||
protobuf_encode<OidbBase<Oidb0xe17Resp>>({
|
||||
body: {
|
||||
jsonBody: JSON.stringify({
|
||||
rpt_block_list: [{ uin: 10001 }, { uin: 10002 }],
|
||||
}),
|
||||
} as any,
|
||||
protobuf_encode<Oidb0xe17Resp>({
|
||||
jsonBody: JSON.stringify({
|
||||
ActionStatus: 'OK',
|
||||
ErrorCode: 0,
|
||||
rpt_block_list: [{ uin: 10001 }, { uin: 10002 }],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -637,10 +637,13 @@ export interface Oidb0x8a7Resp {
|
||||
canAtAll?: pb<6, bool>;
|
||||
}
|
||||
export interface Oidb0xe17Req {
|
||||
subCommand?: pb_optional<2, uint_32>;
|
||||
jsonBody?: pb<3, string>;
|
||||
}
|
||||
export interface Oidb0xe17Resp {
|
||||
errorCode?: pb<3, uint_32>;
|
||||
jsonBody?: pb<4, string>;
|
||||
errorMessage?: pb<5, string>;
|
||||
}
|
||||
export interface Oidb0x112aProfileInfo {
|
||||
tag?: pb<1, uint_32>;
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
// 0xE17_0 — fetch the unidirectional friend block list. Wire name is
|
||||
// non-standard (`MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0`) — uses
|
||||
// the `wireName` override to bypass the default
|
||||
// `OidbSvcTrpcTcp.0xNNNN_N` scheme.
|
||||
//
|
||||
// Request and response carry a JSON-encoded string in a single
|
||||
// protobuf field, so the namespace's `serialize` / `deserialize` do
|
||||
// the JSON marshalling.
|
||||
|
||||
import type { OidbBase } from '@snowluma/proto-defs/oidb';
|
||||
// This service uses its own JSON transport rather than the standard OIDB envelope.
|
||||
import type { Oidb0xe17Req, Oidb0xe17Resp } from '@snowluma/proto-defs/oidb-actions/base';
|
||||
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
|
||||
import type { BridgeContext } from '../../bridge-context';
|
||||
import { invokeOidb, type OidbSender } from '../../oidb-service';
|
||||
import { OidbError, type OidbSender } from '../../oidb-service';
|
||||
import { requirePacketResponse } from '../../msg-push/packet-response';
|
||||
|
||||
export interface UnidirectionalFriendEntry {
|
||||
[key: string]: import('@snowluma/common/json').JsonValue;
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is UnidirectionalFriendEntry {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export namespace GetUnidirectionalFriendList {
|
||||
export const command = 0xe17;
|
||||
export const subCommand = 0;
|
||||
|
||||
/** Wire name override — this cmd lives outside the standard
|
||||
* `OidbSvcTrpcTcp.0xNNNN_N` namespace. */
|
||||
export const wireName = (): string => 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0';
|
||||
|
||||
export interface Params {}
|
||||
@@ -36,26 +30,45 @@ export namespace GetUnidirectionalFriendList {
|
||||
uint32_req_num: 99,
|
||||
bytes_cookies: '',
|
||||
};
|
||||
return { jsonBody: JSON.stringify(reqObj) };
|
||||
return { subCommand, jsonBody: JSON.stringify(reqObj) };
|
||||
};
|
||||
|
||||
export const deserialize = (_ctx: Deps, body: Oidb0xe17Resp): UnidirectionalFriendEntry[] => {
|
||||
if (body.errorCode) {
|
||||
throw new OidbError(body.errorCode, body.errorMessage ?? '', command, subCommand);
|
||||
}
|
||||
|
||||
if (!body || !body.jsonBody) {
|
||||
throw new Error('get unidirectional friend list empty');
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(body.jsonBody) as {
|
||||
rpt_block_list?: UnidirectionalFriendEntry[];
|
||||
};
|
||||
return parsed.rpt_block_list || [];
|
||||
const parsed: unknown = JSON.parse(body.jsonBody);
|
||||
|
||||
if (!isObject(parsed)) {
|
||||
throw new Error('get unidirectional friend list invalid response');
|
||||
}
|
||||
|
||||
if (parsed.ActionStatus !== 'OK' || parsed.ErrorCode !== 0) {
|
||||
const code = typeof parsed.ErrorCode === 'number' ? parsed.ErrorCode : 'missing';
|
||||
const message = typeof parsed.ErrorInfo === 'string' ? parsed.ErrorInfo : '';
|
||||
throw new Error(`get unidirectional friend list failed: ${code} ${message}`.trim());
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed.rpt_block_list) || !parsed.rpt_block_list.every(isObject)) {
|
||||
throw new Error('get unidirectional friend list invalid block list');
|
||||
}
|
||||
|
||||
return parsed.rpt_block_list;
|
||||
};
|
||||
|
||||
export const encode = (env: OidbBase<Oidb0xe17Req>): Uint8Array =>
|
||||
protobuf_encode<OidbBase<Oidb0xe17Req>>(env);
|
||||
export const encode = (request: Oidb0xe17Req): Uint8Array =>
|
||||
protobuf_encode<Oidb0xe17Req>(request);
|
||||
|
||||
export const decode = (bytes: Uint8Array): OidbBase<Oidb0xe17Resp> =>
|
||||
protobuf_decode<OidbBase<Oidb0xe17Resp>>(bytes);
|
||||
export const decode = (bytes: Uint8Array): Oidb0xe17Resp =>
|
||||
protobuf_decode<Oidb0xe17Resp>(bytes);
|
||||
|
||||
export const invoke = (deps: Deps): Promise<UnidirectionalFriendEntry[]> =>
|
||||
invokeOidb(deps, GetUnidirectionalFriendList, {});
|
||||
export const invoke = async (deps: Deps): Promise<UnidirectionalFriendEntry[]> => {
|
||||
const result = await deps.sendRawPacket(wireName(), encode(serialize(deps, {})));
|
||||
return deserialize(deps, decode(requirePacketResponse(result, wireName())));
|
||||
};
|
||||
}
|
||||
|
||||
+77
-69
@@ -1,93 +1,101 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
|
||||
import type { OidbBase } from '@snowluma/proto-defs/oidb';
|
||||
import type { Oidb0xe17Req, Oidb0xe17Resp } from '@snowluma/proto-defs/oidb-actions/base';
|
||||
import type { SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
|
||||
import type { IdentityService } from '../../../src/identity-service';
|
||||
import { GetUnidirectionalFriendList } from '../../../src/oidb-services/profile/get-unidirectional-friend-list';
|
||||
import { s, v } from '../_pb-oracle';
|
||||
|
||||
function makeDeps(responseJson?: unknown) {
|
||||
const responseData =
|
||||
responseJson !== undefined
|
||||
? Buffer.from(
|
||||
protobuf_encode<OidbBase<Oidb0xe17Resp>>({
|
||||
body: { jsonBody: JSON.stringify(responseJson) },
|
||||
}),
|
||||
)
|
||||
: Buffer.alloc(0);
|
||||
const r: SendPacketResult = {
|
||||
function makeDeps(json: unknown = { ActionStatus: 'OK', ErrorCode: 0, rpt_block_list: [] }) {
|
||||
const result: SendPacketResult = {
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData,
|
||||
responseData: Buffer.from([
|
||||
...v(1, 0xe17),
|
||||
...v(2, 0),
|
||||
...v(3, 0),
|
||||
...s(4, JSON.stringify(json)),
|
||||
]),
|
||||
};
|
||||
return {
|
||||
sendRawPacket: vi.fn(async () => r),
|
||||
identity: { uin: '10001' } as any,
|
||||
sendRawPacket: vi.fn(async (_cmd: string, _body: Uint8Array) => result),
|
||||
identity: { uin: '10001' } as IdentityService,
|
||||
};
|
||||
}
|
||||
|
||||
describe('GetUnidirectionalFriendList namespace', () => {
|
||||
it('declares 0xE17_0', () => {
|
||||
expect(GetUnidirectionalFriendList.command).toBe(0xe17);
|
||||
expect(GetUnidirectionalFriendList.subCommand).toBe(0);
|
||||
});
|
||||
|
||||
it('overrides the wire name to MQUpdateSvc.OidbSvc.0xe17_0', async () => {
|
||||
const deps = makeDeps({ rpt_block_list: [] });
|
||||
await GetUnidirectionalFriendList.invoke(deps);
|
||||
expect(deps.sendRawPacket.mock.calls[0]![0]).toBe(
|
||||
'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0',
|
||||
describe('GetUnidirectionalFriendList', () => {
|
||||
it('uses the direct JSON request and response captured for this service', async () => {
|
||||
const list = [{ uint64_uin: '20002', extra: { keep: true } }];
|
||||
const deps = makeDeps({ ActionStatus: 'OK', ErrorCode: 0, rpt_block_list: list });
|
||||
await expect(GetUnidirectionalFriendList.invoke(deps)).resolves.toEqual(list);
|
||||
const [command, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
expect(command).toBe('MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0');
|
||||
expect(Buffer.from(bytes)).toEqual(
|
||||
Buffer.from([
|
||||
...v(2, 0),
|
||||
...s(
|
||||
3,
|
||||
JSON.stringify({
|
||||
uint64_uin: '10001',
|
||||
uint64_top: 0,
|
||||
uint32_req_num: 99,
|
||||
bytes_cookies: '',
|
||||
}),
|
||||
),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
describe('serialize', () => {
|
||||
it('JSON-encodes the request body with the bot uin and fixed pagination', async () => {
|
||||
const deps = makeDeps({ rpt_block_list: [] });
|
||||
await GetUnidirectionalFriendList.invoke(deps);
|
||||
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
const env = protobuf_decode<OidbBase<Oidb0xe17Req>>(bytes);
|
||||
const parsed = JSON.parse(env.body?.jsonBody ?? '{}');
|
||||
expect(parsed).toEqual({
|
||||
uint64_uin: '10001',
|
||||
uint64_top: 0,
|
||||
uint32_req_num: 99,
|
||||
bytes_cookies: '',
|
||||
});
|
||||
});
|
||||
it('returns a verified empty list', async () => {
|
||||
await expect(GetUnidirectionalFriendList.invoke(makeDeps())).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
describe('deserialize', () => {
|
||||
it('parses the embedded JSON body and returns rpt_block_list', () => {
|
||||
const out = GetUnidirectionalFriendList.deserialize({} as any, {
|
||||
jsonBody: JSON.stringify({ rpt_block_list: [{ uin: 10001 }, { uin: 10002 }] }),
|
||||
});
|
||||
expect(out).toEqual([{ uin: 10001 }, { uin: 10002 }]);
|
||||
});
|
||||
|
||||
it('returns [] when rpt_block_list is absent', () => {
|
||||
const out = GetUnidirectionalFriendList.deserialize({} as any, {
|
||||
jsonBody: JSON.stringify({}),
|
||||
});
|
||||
expect(out).toEqual([]);
|
||||
});
|
||||
|
||||
it('throws when jsonBody is empty / missing', () => {
|
||||
expect(() => GetUnidirectionalFriendList.deserialize({} as any, {})).toThrow(
|
||||
'get unidirectional friend list empty',
|
||||
);
|
||||
expect(() =>
|
||||
GetUnidirectionalFriendList.deserialize({} as any, { jsonBody: '' }),
|
||||
).toThrow('get unidirectional friend list empty');
|
||||
it('surfaces JSON business failures instead of returning an empty list', async () => {
|
||||
const deps = makeDeps({
|
||||
ActionStatus: 'FAIL',
|
||||
ErrorCode: 302,
|
||||
ErrorInfo: 'parse json failed',
|
||||
});
|
||||
await expect(GetUnidirectionalFriendList.invoke(deps)).rejects.toThrow(
|
||||
'302 parse json failed',
|
||||
);
|
||||
});
|
||||
|
||||
describe('invoke (e2e)', () => {
|
||||
it('returns the parsed block list', async () => {
|
||||
const deps = makeDeps({ rpt_block_list: [{ uin: 999 }] });
|
||||
const out = await GetUnidirectionalFriendList.invoke(deps);
|
||||
expect(out).toEqual([{ uin: 999 }]);
|
||||
it.each([
|
||||
null,
|
||||
{},
|
||||
{ ActionStatus: 'OK', ErrorCode: 0 },
|
||||
{ ActionStatus: 'OK', ErrorCode: 0, rpt_block_list: [null] },
|
||||
{ ActionStatus: 'OK', ErrorCode: 0, rpt_block_list: {} },
|
||||
])('rejects malformed response data (%j)', async (json) => {
|
||||
await expect(GetUnidirectionalFriendList.invoke(makeDeps(json))).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('surfaces outer errors, invalid JSON, empty replies and transport failures', async () => {
|
||||
const deps = makeDeps();
|
||||
|
||||
for (const [responseData, message] of [
|
||||
[Buffer.from([...v(3, 5), ...s(5, 'denied')]), 'denied'],
|
||||
[Buffer.from(s(4, '{')), 'JSON'],
|
||||
[Buffer.from(v(1, 0xe17)), 'empty'],
|
||||
[Buffer.alloc(0), 'empty'],
|
||||
] as const) {
|
||||
deps.sendRawPacket.mockResolvedValue({
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData,
|
||||
});
|
||||
await expect(GetUnidirectionalFriendList.invoke(deps)).rejects.toThrow(message);
|
||||
}
|
||||
|
||||
deps.sendRawPacket.mockResolvedValue({
|
||||
success: false,
|
||||
gotResponse: false,
|
||||
errorCode: 5,
|
||||
errorMessage: 'disconnected',
|
||||
});
|
||||
await expect(GetUnidirectionalFriendList.invoke(deps)).rejects.toThrow('disconnected');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user