fix(onebot): report rejected group kick operations
Treat group member removal as successful only when QQ accepts the operation. Preserve the rejection reason so callers no longer receive a false success when a target cannot be removed. Apply the same behavior to single-member and batch removal actions, while keeping accepted operations unchanged. Add regression coverage for both successful and rejected responses. Fixes #298
This commit is contained in:
@@ -62,6 +62,9 @@ describe('apis/group-admin', () => {
|
||||
|
||||
it('kickMember resolves UID per-group and forwards reject + reason', async () => {
|
||||
const bridge = mockBridge();
|
||||
bridge.sendRawPacket.mockResolvedValueOnce(packResponse(
|
||||
Buffer.from('220308b960', 'hex'),
|
||||
));
|
||||
await new GroupAdminApi(bridge as any).kickMember(12345, 67890, true, 'bad behaviour');
|
||||
expect(bridge.resolveUserUid).toHaveBeenCalledWith(67890, 12345);
|
||||
const [cmd, bytes] = bridge.sendRawPacket.mock.calls[0]!;
|
||||
@@ -75,6 +78,20 @@ describe('apis/group-admin', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('kickMember rejects a server business error instead of reporting success (#298)', async () => {
|
||||
const bridge = mockBridge();
|
||||
bridge.sendRawPacket.mockResolvedValueOnce(packResponse(
|
||||
Buffer.from(
|
||||
'222008b960121be7bea4e4b8bbe697a0e6b395e8a2abe7a7bbe587bae7bea4e8818a',
|
||||
'hex',
|
||||
),
|
||||
));
|
||||
|
||||
await expect(
|
||||
new GroupAdminApi(bridge as any).kickMember(12345, 67890, false),
|
||||
).rejects.toThrow('群主无法被移出群聊');
|
||||
});
|
||||
|
||||
it('kickMembers resolves each UID in parallel', async () => {
|
||||
const bridge = mockBridge();
|
||||
vi.mocked(bridge.resolveUserUid)
|
||||
@@ -88,6 +105,20 @@ describe('apis/group-admin', () => {
|
||||
expect(env.body?.rejectAddRequest ?? 0).toBe(0);
|
||||
});
|
||||
|
||||
it('kickMembers rejects the same command-level business error', async () => {
|
||||
const bridge = mockBridge();
|
||||
bridge.sendRawPacket.mockResolvedValueOnce(packResponse(
|
||||
Buffer.from(
|
||||
'222008b960121be7bea4e4b8bbe697a0e6b395e8a2abe7a7bbe587bae7bea4e8818a',
|
||||
'hex',
|
||||
),
|
||||
));
|
||||
|
||||
await expect(
|
||||
new GroupAdminApi(bridge as any).kickMembers(12345, [67890], false),
|
||||
).rejects.toThrow('群主无法被移出群聊');
|
||||
});
|
||||
|
||||
it('leave sends 0x1097_1, emits a self group_member_leave, and forgets the group (#133)', async () => {
|
||||
const bridge = mockBridge();
|
||||
await new GroupAdminApi(bridge as any).leave(12345);
|
||||
|
||||
@@ -62,6 +62,14 @@ export interface OidbKickMember {
|
||||
rejectAddRequest?: pb<4, bool>;
|
||||
reason?: pb<5, string>;
|
||||
}
|
||||
// 0x8A0_1 response body. Cross-checked against Lagrange.Core's
|
||||
// OidbSvcTrpcTcp0x8A0_1Response and the current QQ kick result contract:
|
||||
// transport/envelope success does not imply the member was removed; a
|
||||
// command-level refusal is returned in errorMsg.
|
||||
export interface OidbKickMemberResponse {
|
||||
groupUin?: pb<1, uint_32>;
|
||||
errorMsg?: pb<2, string>;
|
||||
}
|
||||
export interface OidbLeaveGroup {
|
||||
groupUin?: pb<1, uint_32>;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
// namespace because proton needs distinct types per call site.
|
||||
|
||||
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
|
||||
import type { OidbBase, OidbEmpty } from '@snowluma/proto-defs/oidb';
|
||||
import type { OidbKickMember } from '@snowluma/proto-defs/oidb-actions/base';
|
||||
import type { OidbBase } from '@snowluma/proto-defs/oidb';
|
||||
import type {
|
||||
OidbKickMember,
|
||||
OidbKickMemberResponse,
|
||||
} from '@snowluma/proto-defs/oidb-actions/base';
|
||||
import { invokeOidb, type OidbSender } from '../../oidb-service';
|
||||
import type { BridgeContext } from '../../bridge-context';
|
||||
|
||||
@@ -29,13 +32,17 @@ export namespace KickMember {
|
||||
reason: p.reason ?? '',
|
||||
});
|
||||
|
||||
export const deserialize = (_ctx: Deps, _: OidbEmpty): void => {};
|
||||
export const deserialize = (_ctx: Deps, body: OidbKickMemberResponse): void => {
|
||||
if (body.errorMsg) {
|
||||
throw new Error(`kick member failed: ${body.errorMsg}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const encode = (env: OidbBase<OidbKickMember>): Uint8Array =>
|
||||
protobuf_encode<OidbBase<OidbKickMember>>(env);
|
||||
|
||||
export const decode = (bytes: Uint8Array): OidbBase<OidbEmpty> =>
|
||||
protobuf_decode<OidbBase<OidbEmpty>>(bytes);
|
||||
export const decode = (bytes: Uint8Array): OidbBase<OidbKickMemberResponse> =>
|
||||
protobuf_decode<OidbBase<OidbKickMemberResponse>>(bytes);
|
||||
|
||||
export const invoke = (deps: Deps, params: Params): Promise<void> =>
|
||||
invokeOidb(deps, KickMember, params);
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
// namespace so proton can monomorphize independently.
|
||||
|
||||
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
|
||||
import type { OidbBase, OidbEmpty } from '@snowluma/proto-defs/oidb';
|
||||
import type { Oidb0x8a0Req } from '@snowluma/proto-defs/oidb-actions/base';
|
||||
import type { OidbBase } from '@snowluma/proto-defs/oidb';
|
||||
import type {
|
||||
Oidb0x8a0Req,
|
||||
OidbKickMemberResponse,
|
||||
} from '@snowluma/proto-defs/oidb-actions/base';
|
||||
import { invokeOidb, type OidbSender } from '../../oidb-service';
|
||||
import type { BridgeContext } from '../../bridge-context';
|
||||
|
||||
@@ -28,13 +31,17 @@ export namespace KickMembers {
|
||||
field12: 0,
|
||||
});
|
||||
|
||||
export const deserialize = (_ctx: Deps, _: OidbEmpty): void => {};
|
||||
export const deserialize = (_ctx: Deps, body: OidbKickMemberResponse): void => {
|
||||
if (body.errorMsg) {
|
||||
throw new Error(`kick members failed: ${body.errorMsg}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const encode = (env: OidbBase<Oidb0x8a0Req>): Uint8Array =>
|
||||
protobuf_encode<OidbBase<Oidb0x8a0Req>>(env);
|
||||
|
||||
export const decode = (bytes: Uint8Array): OidbBase<OidbEmpty> =>
|
||||
protobuf_decode<OidbBase<OidbEmpty>>(bytes);
|
||||
export const decode = (bytes: Uint8Array): OidbBase<OidbKickMemberResponse> =>
|
||||
protobuf_decode<OidbBase<OidbKickMemberResponse>>(bytes);
|
||||
|
||||
export const invoke = (deps: Deps, params: Params): Promise<void> =>
|
||||
invokeOidb(deps, KickMembers, params);
|
||||
|
||||
Reference in New Issue
Block a user