feat(onebot): read administrator verification message preference
Include verification message reception in administrator permission results. Preserve unavailable states and surface failed reads. Update SDK contracts, action metadata, documentation and isolated behavior coverage.
This commit is contained in:
@@ -9,6 +9,7 @@ import { SetAddOption } from '@snowluma/protocol/oidb-services/group-admin/set-a
|
||||
import { SetAddRequest } from '@snowluma/protocol/oidb-services/group-admin/set-add-request';
|
||||
import { SetAdmin } from '@snowluma/protocol/oidb-services/group-admin/set-admin';
|
||||
import { GetAdminPermissions } from '@snowluma/protocol/oidb-services/group-admin/get-admin-permissions';
|
||||
import { GetAdminReceiveVerify } from '@snowluma/protocol/oidb-services/group-admin/get-admin-receive-verify';
|
||||
import {
|
||||
SetAdminPermissions,
|
||||
type GroupAdminPermissionPatch,
|
||||
@@ -355,14 +356,18 @@ export class GroupAdminApi {
|
||||
async getAdminPermissions(groupId: number, userId: number) {
|
||||
const logger = log.child({ uin: Number(this.ctx.identity.uin) });
|
||||
logger.debug('Reading administrator permissions group=%s user=%s', groupId, userId);
|
||||
const snapshot = await GetAdminPermissions.invoke(this.ctx, { groupId, userId });
|
||||
const [snapshot, receiveVerify] = await Promise.all([
|
||||
GetAdminPermissions.invoke(this.ctx, { groupId, userId }),
|
||||
GetAdminReceiveVerify.invoke(this.ctx, { groupId, userId }),
|
||||
]);
|
||||
logger.debug(
|
||||
'Administrator permissions received group=%s user=%s recall=%s kick=%s limit=%s',
|
||||
'Administrator permissions received group=%s user=%s recall=%s kick=%s limit=%s receiveVerify=%s',
|
||||
groupId,
|
||||
userId,
|
||||
snapshot.recallMemberMessage,
|
||||
snapshot.kickMember,
|
||||
snapshot.kickMemberPerDay,
|
||||
receiveVerify,
|
||||
);
|
||||
return {
|
||||
group_id: groupId,
|
||||
@@ -370,6 +375,7 @@ export class GroupAdminApi {
|
||||
recall_member_message: snapshot.recallMemberMessage,
|
||||
kick_member: snapshot.kickMember,
|
||||
kick_member_per_day: snapshot.kickMemberPerDay,
|
||||
receive_verify: receiveVerify,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { OidbBase, OidbSvcTrpcTcp0x88D_0Response } from '@snowluma/proto-de
|
||||
import type { OidbGetGroupExtResp } from '@snowluma/proto-defs/oidb-actions/group-ext';
|
||||
import type {
|
||||
GetGroupAdminPermissionsResponse,
|
||||
GetGroupAdminReceiveVerifyResponse,
|
||||
SetGroupAdminPermissionsRequest,
|
||||
SetGroupAdminReceiveVerifyRequest,
|
||||
SetGroupAdminReceiveVerifyResponse,
|
||||
@@ -120,34 +121,112 @@ function packRobotExt(robotMemberSwitch?: number, robotMemberExamine?: number) {
|
||||
}
|
||||
|
||||
describe('apis/group-admin', () => {
|
||||
it('maps the administrator snapshot without inventing missing permissions', async () => {
|
||||
it.each([
|
||||
[0, true],
|
||||
[64, false],
|
||||
[undefined, null],
|
||||
] as const)(
|
||||
'maps receive-verification state %s without inventing missing permissions',
|
||||
async (flag, enabled) => {
|
||||
const bridge = mockBridge();
|
||||
bridge.sendRawPacket.mockResolvedValueOnce(
|
||||
packResponse(
|
||||
protobuf_encode<OidbBase<GetGroupAdminPermissionsResponse>>({
|
||||
body: {
|
||||
groupCode: 12345n,
|
||||
members: [
|
||||
{
|
||||
memberUin: 67890n,
|
||||
ext: { permissions: { items: [{ item: 1, value: 2 }] } },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
bridge.sendRawPacket.mockResolvedValueOnce(
|
||||
packResponse(
|
||||
protobuf_encode<OidbBase<GetGroupAdminReceiveVerifyResponse>>({
|
||||
body: {
|
||||
groupCode: 12345n,
|
||||
members: [
|
||||
{
|
||||
identity: { uid: 'resolved-uid', uin: 67890n },
|
||||
memberFlagExt: flag,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expect(
|
||||
new GroupAdminApi(bridge as unknown as BridgeContext).getAdminPermissions(
|
||||
12345,
|
||||
67890,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
group_id: 12345,
|
||||
user_id: 67890,
|
||||
recall_member_message: null,
|
||||
kick_member: false,
|
||||
kick_member_per_day: null,
|
||||
receive_verify: enabled,
|
||||
});
|
||||
expect(bridge.sendRawPacket).toHaveBeenCalledTimes(2);
|
||||
expect(bridge.resolveUserUid).toHaveBeenCalledExactlyOnceWith(67890, 12345);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['OidbSvcTrpcTcp.0x9164_2', 'OidbSvcTrpcTcp.0xfe7_4'])(
|
||||
'rejects the combined snapshot when %s fails',
|
||||
async (failedCommand) => {
|
||||
const bridge = mockBridge();
|
||||
bridge.sendRawPacket.mockImplementation(async (command: string) => {
|
||||
if (command === failedCommand) {
|
||||
return packOidbFail(1287);
|
||||
}
|
||||
|
||||
if (command === 'OidbSvcTrpcTcp.0x9164_2') {
|
||||
return packResponse(
|
||||
protobuf_encode<OidbBase<GetGroupAdminPermissionsResponse>>({
|
||||
body: { groupCode: 12345n, members: [{ memberUin: 67890n }] },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return packResponse(
|
||||
protobuf_encode<OidbBase<GetGroupAdminReceiveVerifyResponse>>({
|
||||
body: {
|
||||
groupCode: 12345n,
|
||||
members: [{ identity: { uid: 'resolved-uid' }, memberFlagExt: 0 }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
await expect(
|
||||
new GroupAdminApi(bridge as unknown as BridgeContext).getAdminPermissions(
|
||||
12345,
|
||||
67890,
|
||||
),
|
||||
).rejects.toThrow('OIDB error 1287');
|
||||
expect(bridge.sendRawPacket).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
);
|
||||
|
||||
it('propagates identity resolution failures when reading administrator permissions', async () => {
|
||||
const bridge = mockBridge();
|
||||
bridge.resolveUserUid.mockRejectedValueOnce(new Error('identity unavailable'));
|
||||
bridge.sendRawPacket.mockResolvedValueOnce(
|
||||
packResponse(
|
||||
protobuf_encode<OidbBase<GetGroupAdminPermissionsResponse>>({
|
||||
body: {
|
||||
groupCode: 12345n,
|
||||
members: [
|
||||
{
|
||||
memberUin: 67890n,
|
||||
ext: { permissions: { items: [{ item: 1, value: 2 }] } },
|
||||
},
|
||||
],
|
||||
},
|
||||
body: { groupCode: 12345n, members: [{ memberUin: 67890n }] },
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expect(
|
||||
new GroupAdminApi(bridge as unknown as BridgeContext).getAdminPermissions(12345, 67890),
|
||||
).resolves.toEqual({
|
||||
group_id: 12345,
|
||||
user_id: 67890,
|
||||
recall_member_message: null,
|
||||
kick_member: false,
|
||||
kick_member_per_day: null,
|
||||
});
|
||||
).rejects.toThrow('identity unavailable');
|
||||
expect(bridge.sendRawPacket).toHaveBeenCalledTimes(1);
|
||||
expect(bridge.resolveUserUid).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('writes administrator capabilities and receive-verification through separate services', async () => {
|
||||
|
||||
@@ -6513,7 +6513,7 @@
|
||||
{
|
||||
"name": "get_group_admin_permissions",
|
||||
"aliases": [],
|
||||
"summary": "读取指定管理员的撤回、移除成员及每日移除上限",
|
||||
"summary": "读取指定管理员的撤回、移除成员、每日移除上限及接收验证消息状态",
|
||||
"returns": "服务器返回的管理员权限;未返回或未指定的开关为 null。每日上限仅在移除权限开启时返回,0 表示不限。",
|
||||
"returnsSchema": {
|
||||
"type": "object",
|
||||
@@ -6544,6 +6544,13 @@
|
||||
"null"
|
||||
],
|
||||
"description": "每日可移除人数,0 不限;权限关闭或未指定时为 null"
|
||||
},
|
||||
"receive_verify": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"description": "是否接收加群验证消息;null 表示服务器未返回状态"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -6551,7 +6558,8 @@
|
||||
"user_id",
|
||||
"recall_member_message",
|
||||
"kick_member",
|
||||
"kick_member_per_day"
|
||||
"kick_member_per_day",
|
||||
"receive_verify"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -6562,7 +6570,7 @@
|
||||
"group_id": 20001,
|
||||
"user_id": 10001
|
||||
},
|
||||
"notes": "user_id 为目标管理员 QQ 号;不包含接收验证消息开关。"
|
||||
"notes": "user_id 为目标管理员 QQ 号;包含接收加群验证消息状态。"
|
||||
}
|
||||
],
|
||||
"readOnly": true,
|
||||
|
||||
@@ -6516,7 +6516,7 @@ export const ACTIONS: CatalogAction[] = [
|
||||
{
|
||||
"name": "get_group_admin_permissions",
|
||||
"aliases": [],
|
||||
"summary": "读取指定管理员的撤回、移除成员及每日移除上限",
|
||||
"summary": "读取指定管理员的撤回、移除成员、每日移除上限及接收验证消息状态",
|
||||
"returns": "服务器返回的管理员权限;未返回或未指定的开关为 null。每日上限仅在移除权限开启时返回,0 表示不限。",
|
||||
"returnsSchema": {
|
||||
"type": "object",
|
||||
@@ -6547,6 +6547,13 @@ export const ACTIONS: CatalogAction[] = [
|
||||
"null"
|
||||
],
|
||||
"description": "每日可移除人数,0 不限;权限关闭或未指定时为 null"
|
||||
},
|
||||
"receive_verify": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"description": "是否接收加群验证消息;null 表示服务器未返回状态"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -6554,7 +6561,8 @@ export const ACTIONS: CatalogAction[] = [
|
||||
"user_id",
|
||||
"recall_member_message",
|
||||
"kick_member",
|
||||
"kick_member_per_day"
|
||||
"kick_member_per_day",
|
||||
"receive_verify"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -6565,7 +6573,7 @@ export const ACTIONS: CatalogAction[] = [
|
||||
"group_id": 20001,
|
||||
"user_id": 10001
|
||||
},
|
||||
"notes": "user_id 为目标管理员 QQ 号;不包含接收验证消息开关。"
|
||||
"notes": "user_id 为目标管理员 QQ 号;包含接收加群验证消息状态。"
|
||||
}
|
||||
],
|
||||
"readOnly": true,
|
||||
|
||||
@@ -468,13 +468,13 @@ export const actions = [
|
||||
|
||||
groupUserAction({
|
||||
name: 'get_group_admin_permissions',
|
||||
summary: '读取指定管理员的撤回、移除成员及每日移除上限',
|
||||
summary: '读取指定管理员的撤回、移除成员、每日移除上限及接收验证消息状态',
|
||||
readOnly: true,
|
||||
examples: [
|
||||
{
|
||||
summary: '读取管理员权限',
|
||||
params: { group_id: 20001, user_id: 10001 },
|
||||
notes: 'user_id 为目标管理员 QQ 号;不包含接收验证消息开关。',
|
||||
notes: 'user_id 为目标管理员 QQ 号;包含接收加群验证消息状态。',
|
||||
},
|
||||
],
|
||||
returns:
|
||||
@@ -496,6 +496,10 @@ export const actions = [
|
||||
type: ['integer', 'null'],
|
||||
description: '每日可移除人数,0 不限;权限关闭或未指定时为 null',
|
||||
},
|
||||
receive_verify: {
|
||||
type: ['boolean', 'null'],
|
||||
description: '是否接收加群验证消息;null 表示服务器未返回状态',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'group_id',
|
||||
@@ -503,6 +507,7 @@ export const actions = [
|
||||
'recall_member_message',
|
||||
'kick_member',
|
||||
'kick_member_per_day',
|
||||
'receive_verify',
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
|
||||
@@ -1053,23 +1053,27 @@ describe('per-administrator permissions', () => {
|
||||
expect(set).toHaveBeenCalledExactlyOnceWith(12345, 67890, false);
|
||||
});
|
||||
|
||||
it('returns nullable permissions without defaulting them to true', async () => {
|
||||
const snapshot = {
|
||||
...target,
|
||||
recall_member_message: null,
|
||||
kick_member: true,
|
||||
kick_member_per_day: 0,
|
||||
};
|
||||
const get = vi.fn(async () => snapshot);
|
||||
const { ctx } = adminCtx({ getAdminPermissions: get });
|
||||
await expect(getAdminPermissions.toHandler(ctx)(target)).resolves.toEqual({
|
||||
status: 'ok',
|
||||
retcode: 0,
|
||||
data: snapshot,
|
||||
});
|
||||
expect(get).toHaveBeenCalledExactlyOnceWith(12345, 67890);
|
||||
expect(getAdminPermissions.describe().readOnly).toBe(true);
|
||||
});
|
||||
it.each([true, false, null])(
|
||||
'returns receive-verification state %s and nullable permissions',
|
||||
async (receiveVerify) => {
|
||||
const snapshot = {
|
||||
...target,
|
||||
recall_member_message: null,
|
||||
kick_member: true,
|
||||
kick_member_per_day: 0,
|
||||
receive_verify: receiveVerify,
|
||||
};
|
||||
const get = vi.fn(async () => snapshot);
|
||||
const { ctx } = adminCtx({ getAdminPermissions: get });
|
||||
await expect(getAdminPermissions.toHandler(ctx)(target)).resolves.toEqual({
|
||||
status: 'ok',
|
||||
retcode: 0,
|
||||
data: snapshot,
|
||||
});
|
||||
expect(get).toHaveBeenCalledExactlyOnceWith(12345, 67890);
|
||||
expect(getAdminPermissions.describe().readOnly).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it('propagates a rejected or unconfirmed update', async () => {
|
||||
const set = vi.fn(async () => {
|
||||
|
||||
@@ -63,3 +63,30 @@ export interface SetGroupAdminReceiveVerifyResponse {
|
||||
groupCode?: pb<1, uint_64>;
|
||||
result?: pb<2, GroupAdminReceiveVerifyResult>;
|
||||
}
|
||||
|
||||
export interface GroupAdminReceiveVerifyIdentity {
|
||||
uid?: pb<2, string>;
|
||||
uin?: pb_optional<4, uint_64>;
|
||||
}
|
||||
|
||||
export interface GroupAdminReceiveVerifyFilter {
|
||||
memberFlagExt?: pb<104, uint_32>;
|
||||
}
|
||||
|
||||
export interface GetGroupAdminReceiveVerifyRequest {
|
||||
groupCode?: pb<1, uint_64>;
|
||||
field2?: pb<2, uint_32>;
|
||||
field3?: pb_optional<3, uint_32>;
|
||||
filter?: pb<4, GroupAdminReceiveVerifyFilter>;
|
||||
members?: pb_repeated<5, GroupAdminReceiveVerifyIdentity>;
|
||||
}
|
||||
|
||||
export interface GroupAdminReceiveVerifyMember {
|
||||
identity?: pb<1, GroupAdminReceiveVerifyIdentity>;
|
||||
memberFlagExt?: pb_optional<104, uint_32>;
|
||||
}
|
||||
|
||||
export interface GetGroupAdminReceiveVerifyResponse {
|
||||
groupCode?: pb<1, uint_64>;
|
||||
members?: pb_repeated<2, GroupAdminReceiveVerifyMember>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
|
||||
import type { OidbBase } from '@snowluma/proto-defs/oidb';
|
||||
import type {
|
||||
GetGroupAdminReceiveVerifyRequest,
|
||||
GetGroupAdminReceiveVerifyResponse,
|
||||
} from '@snowluma/proto-defs/oidb-actions/group-admin-permissions';
|
||||
import type { BridgeContext } from '../../bridge-context';
|
||||
import { invokeOidb, type OidbSender } from '../../oidb-service';
|
||||
|
||||
export namespace GetAdminReceiveVerify {
|
||||
export const command = 0xfe7;
|
||||
export const subCommand = 4;
|
||||
export const uinForm = false;
|
||||
export type Deps = OidbSender & Pick<BridgeContext, 'resolveUserUid'>;
|
||||
export interface Params {
|
||||
groupId: number;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
interface RequestParams {
|
||||
groupId: number;
|
||||
userUid: string;
|
||||
}
|
||||
|
||||
export const serialize = (_ctx: Deps, p: RequestParams): GetGroupAdminReceiveVerifyRequest => ({
|
||||
groupCode: BigInt(p.groupId),
|
||||
field2: 3,
|
||||
field3: 0,
|
||||
filter: { memberFlagExt: 1 },
|
||||
members: [{ uid: p.userUid }],
|
||||
});
|
||||
export const deserialize = (
|
||||
_ctx: Deps,
|
||||
body: GetGroupAdminReceiveVerifyResponse,
|
||||
): GetGroupAdminReceiveVerifyResponse => body;
|
||||
export const encode = (env: OidbBase<GetGroupAdminReceiveVerifyRequest>): Uint8Array =>
|
||||
protobuf_encode<OidbBase<GetGroupAdminReceiveVerifyRequest>>(env);
|
||||
export const decode = (bytes: Uint8Array): OidbBase<GetGroupAdminReceiveVerifyResponse> =>
|
||||
protobuf_decode<OidbBase<GetGroupAdminReceiveVerifyResponse>>(bytes);
|
||||
|
||||
export async function invoke(deps: Deps, params: Params): Promise<boolean | null> {
|
||||
const userUid = await deps.resolveUserUid(params.userId, params.groupId);
|
||||
const body = await invokeOidb(deps, GetAdminReceiveVerify, {
|
||||
groupId: params.groupId,
|
||||
userUid,
|
||||
});
|
||||
|
||||
if (body.groupCode !== BigInt(params.groupId)) {
|
||||
throw new Error(
|
||||
'administrator receive-verify response has a missing or mismatched group',
|
||||
);
|
||||
}
|
||||
|
||||
const members = (body.members ?? []).filter((member) => member.identity?.uid === userUid);
|
||||
|
||||
if (members.length !== 1) {
|
||||
throw new Error(
|
||||
'administrator receive-verify response must contain exactly one matching member',
|
||||
);
|
||||
}
|
||||
|
||||
const member = members[0]!;
|
||||
const returnedUin = member.identity?.uin;
|
||||
|
||||
if (returnedUin != null && returnedUin !== BigInt(params.userId)) {
|
||||
throw new Error('administrator receive-verify response has a mismatched member UIN');
|
||||
}
|
||||
|
||||
// Missing data must remain unknown; an explicit zero is a known enabled state.
|
||||
return member.memberFlagExt == null ? null : (member.memberFlagExt & 64) === 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
import { GetAdminReceiveVerify } from '../../../src/oidb-services/group-admin/get-admin-receive-verify';
|
||||
import { env, m, s, v } from '../_pb-oracle';
|
||||
|
||||
const target = { groupId: 12345, userId: 67890 };
|
||||
const uid = 'test-member-uid';
|
||||
|
||||
function member(flag?: number, memberUid = uid, uin: number | undefined = target.userId) {
|
||||
return m(2, [
|
||||
...m(1, [...s(2, memberUid), ...(uin === undefined ? [] : v(4, uin))]),
|
||||
...(flag === undefined ? [] : v(104, flag)),
|
||||
]);
|
||||
}
|
||||
|
||||
function response(members: number[], group: number | undefined = target.groupId) {
|
||||
return m(4, [...(group === undefined ? [] : v(1, group)), ...members]);
|
||||
}
|
||||
|
||||
function sender(body = response(member(0))) {
|
||||
const result: SendPacketResult = {
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.from(body),
|
||||
};
|
||||
return {
|
||||
sendRawPacket: vi.fn(async (_command: string, _bytes: Uint8Array) => result),
|
||||
resolveUserUid: vi.fn(async (_userId: number, _groupId?: number) => uid),
|
||||
};
|
||||
}
|
||||
|
||||
describe('administrator receive-verification read', () => {
|
||||
it('resolves the member once in its group and requests only the verification state', async () => {
|
||||
const deps = sender();
|
||||
await expect(GetAdminReceiveVerify.invoke(deps, target)).resolves.toBe(true);
|
||||
expect(deps.resolveUserUid).toHaveBeenCalledExactlyOnceWith(target.userId, target.groupId);
|
||||
expect(deps.sendRawPacket).toHaveBeenCalledTimes(1);
|
||||
const [command, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
expect(command).toBe('OidbSvcTrpcTcp.0xfe7_4');
|
||||
expect(Buffer.from(bytes).toString('hex')).toBe(
|
||||
env(
|
||||
0xfe7,
|
||||
4,
|
||||
[
|
||||
...v(1, target.groupId),
|
||||
...v(2, 3),
|
||||
...v(3, 0),
|
||||
...m(4, v(104, 1)),
|
||||
...m(5, s(2, uid)),
|
||||
],
|
||||
false,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[0, true],
|
||||
[64, false],
|
||||
[32, true],
|
||||
[96, false],
|
||||
[0x80000000, true],
|
||||
[0xffffffff, false],
|
||||
[undefined, null],
|
||||
] as const)('decodes flag %s as %s while preserving missing data', async (flag, enabled) => {
|
||||
const deps = sender(response(member(flag)));
|
||||
await expect(GetAdminReceiveVerify.invoke(deps, target)).resolves.toBe(enabled);
|
||||
});
|
||||
|
||||
it('selects the requested member even when unrelated members are returned first', async () => {
|
||||
const deps = sender(response([...member(64, 'unrelated-member', 10001), ...member(0)]));
|
||||
await expect(GetAdminReceiveVerify.invoke(deps, target)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('accepts UID-bound responses without an optional numeric identity', async () => {
|
||||
const deps = sender(response(m(2, [...m(1, s(2, uid)), ...v(104, 64)])));
|
||||
await expect(GetAdminReceiveVerify.invoke(deps, target)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('preserves large numeric group and member identifiers', async () => {
|
||||
const large = { groupId: 2 ** 40 + 17, userId: 2 ** 41 + 19 };
|
||||
const deps = sender(response(member(0, uid, large.userId), large.groupId));
|
||||
await expect(GetAdminReceiveVerify.invoke(deps, large)).resolves.toBe(true);
|
||||
expect(Buffer.from(deps.sendRawPacket.mock.calls[0]![1]).toString('hex')).toBe(
|
||||
env(
|
||||
0xfe7,
|
||||
4,
|
||||
[
|
||||
...v(1, large.groupId),
|
||||
...v(2, 3),
|
||||
...v(3, 0),
|
||||
...m(4, v(104, 1)),
|
||||
...m(5, s(2, uid)),
|
||||
],
|
||||
false,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['missing group', m(4, member(0)), 'missing or mismatched group'],
|
||||
['wrong group', response(member(0), 999), 'missing or mismatched group'],
|
||||
['missing member', response([]), 'exactly one matching member'],
|
||||
['wrong member', response(member(0, 'other-uid')), 'exactly one matching member'],
|
||||
[
|
||||
'missing UID',
|
||||
response(m(2, [...m(1, v(4, target.userId)), ...v(104, 0)])),
|
||||
'exactly one matching member',
|
||||
],
|
||||
[
|
||||
'duplicate member',
|
||||
response([...member(0), ...member(64)]),
|
||||
'exactly one matching member',
|
||||
],
|
||||
['conflicting numeric identity', response(member(0, uid, 999)), 'mismatched member UIN'],
|
||||
] as const)(
|
||||
'rejects %s rather than reporting a successful state',
|
||||
async (_label, body, error) => {
|
||||
const deps = sender([...body]);
|
||||
await expect(GetAdminReceiveVerify.invoke(deps, target)).rejects.toThrow(error);
|
||||
},
|
||||
);
|
||||
|
||||
it('does not send when identity resolution fails', async () => {
|
||||
const deps = sender();
|
||||
deps.resolveUserUid.mockRejectedValueOnce(new Error('identity unavailable'));
|
||||
await expect(GetAdminReceiveVerify.invoke(deps, target)).rejects.toThrow(
|
||||
'identity unavailable',
|
||||
);
|
||||
expect(deps.sendRawPacket).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('propagates server rejection even if the body contains a valid state', async () => {
|
||||
const deps = sender([...v(3, 1287), ...response(member(0))]);
|
||||
await expect(GetAdminReceiveVerify.invoke(deps, target)).rejects.toThrow('OIDB error 1287');
|
||||
});
|
||||
|
||||
it('propagates transport failures without retrying', async () => {
|
||||
const deps = sender();
|
||||
deps.sendRawPacket.mockRejectedValueOnce(new Error('transport disconnected'));
|
||||
await expect(GetAdminReceiveVerify.invoke(deps, target)).rejects.toThrow(
|
||||
'transport disconnected',
|
||||
);
|
||||
expect(deps.sendRawPacket).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rejects truncated responses', async () => {
|
||||
const deps = sender([0x22, 0x80]);
|
||||
await expect(GetAdminReceiveVerify.invoke(deps, target)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -233,7 +233,7 @@ if (feed) {
|
||||
|
||||
| 动作 | 参数与结果 |
|
||||
| --- | --- |
|
||||
| `get_group_admin_permissions` | `group_id`、`user_id`;返回撤回权限、移除权限和每日移除上限 |
|
||||
| `get_group_admin_permissions` | `group_id`、`user_id`;返回撤回权限、移除权限、每日移除上限和接收验证消息状态 |
|
||||
| `set_group_admin_permissions` | `group_id`、`user_id`,以及至少一个 `recall_member_message` / `kick_member` |
|
||||
| `set_group_admin_receive_verify` | `group_id`、`user_id`、必填 `enable`;独立设置接收加群验证消息 |
|
||||
|
||||
@@ -260,7 +260,7 @@ await bot.raw('set_group_admin_receive_verify', {
|
||||
|
||||
只修改显式提供的权限。开启移除权限必须明确提供 `kick_member_per_day`,支持 `0`、`5`、`10`、`20`、`50`、`100`、`200`,其中 `0` 表示不限。修改上限也须同时传 `kick_member: true`;关闭或省略移除权限时不能提供上限。SDK 的 `kick_member` 使用布尔值,以便类型检查约束上限。
|
||||
|
||||
读取结果包含 `group_id`、`user_id`、`recall_member_message`、`kick_member`、`kick_member_per_day`。服务器未指定的开关返回 `null`;移除权限关闭或未指定时,上限也为 `null`。当前读取接口不返回接收验证消息状态。
|
||||
读取结果包含 `group_id`、`user_id`、`recall_member_message`、`kick_member`、`kick_member_per_day`、`receive_verify`。`receive_verify` 表示是否接收加群验证消息。服务器未返回或未指定的状态为 `null`;移除权限关闭或未指定时,上限也为 `null`。各项来自独立查询,不保证同一时刻的状态;任何查询失败都会报错。
|
||||
|
||||
两个写动作分别发送一次请求,不自动重试或授予管理员身份。权限写入成功表示服务器确认请求;接收验证消息写入还会核对响应中的群号和成功管理员列表。两者均返回 `null`。完整的客户端显示和实际权限效果仍需真实 QQ 验证,不能以接口成功替代验收。
|
||||
|
||||
|
||||
@@ -278,6 +278,8 @@ export interface GroupAdminPermissions {
|
||||
kick_member: boolean | null;
|
||||
/** Zero means unlimited; null means removal is disabled or unspecified. */
|
||||
kick_member_per_day: number | null;
|
||||
/** Whether join-verification messages are received; null means the state was not returned. */
|
||||
receive_verify: boolean | null;
|
||||
}
|
||||
|
||||
export interface GroupTodoListItem {
|
||||
|
||||
@@ -286,6 +286,7 @@ export const actionResults = {
|
||||
recall_member_message: null,
|
||||
kick_member: true,
|
||||
kick_member_per_day: 20,
|
||||
receive_verify: false,
|
||||
},
|
||||
set_group_admin_permissions: null,
|
||||
set_group_admin_receive_verify: null,
|
||||
|
||||
@@ -49,6 +49,7 @@ const adminPermissions = await http.raw('get_group_admin_permissions', {
|
||||
});
|
||||
export const adminRecall: boolean | null = adminPermissions.recall_member_message;
|
||||
export const adminKickLimit: number | null = adminPermissions.kick_member_per_day;
|
||||
export const adminReceiveVerify: boolean | null = adminPermissions.receive_verify;
|
||||
await http.raw('set_group_admin_permissions', {
|
||||
group_id: 10001,
|
||||
user_id: 10002,
|
||||
|
||||
Reference in New Issue
Block a user