diff --git a/docs/api/v2.md b/docs/api/v2.md index ccae897..40effeb 100644 --- a/docs/api/v2.md +++ b/docs/api/v2.md @@ -11,7 +11,9 @@ | `post_group_message(group_openid, content="", msg_id=None, msg_seq=1, media=None, event_id=None, markdown=None, keyboard=None, **extra)` | POST /v2/groups/{group_openid}/messages | `msg_id`/`event_id` 二选一即被动回复(`event_id` 支持 GROUP_ADD_ROBOT / INTERACTION_CREATE 等事件);`media` 传上传返回的 `{"file_info": …}`(自动 msg_type=7);`markdown` 为字符串或完整 dict(自动 msg_type=2,与 content 互斥);`keyboard` 为内嵌键盘 `{"content": {"rows": [...]}}` 或 `{"id": …}`;其余字段经 `extra` 透传 | | `upload_group_file(group_openid, file_type, url, srv_send_msg=False)` | POST /v2/groups/{group_openid}/files | 富媒体上传,返回含 `file_info`;`srv_send_msg=True` 上传即发送 | | `withdraw_group_message(group_openid, message_id)` | DELETE /v2/groups/{group_openid}/messages/{message_id} | 发出 2 分钟内可撤回 | -| `mute_group_member(group_openid, mute_expire_at)` | POST /v2/groups/{group_openid}/restrict_chat_setting | `mute_expire_at` 为 RFC3339 到期时间;传早于当前的时间即解禁 | +| `mute_group_members(group_openid, members)` | POST /v2/groups/{group_openid}/restrict_chat_setting | 批量禁言/解禁;`members` 每项为 `{"op": "add"/"update"/"del", "member_openid": …, "mute_expire_at": …}`(RFC3339):add 新增禁言、update 更新到期时间、del 解禁(expire 可传空串)。单次最多 20 人、最长禁言 30 天、仅可操作普通成员(群主/管理员/机器人不可);需群管理员身份,60 QPM | +| `mute_group_member(group_openid, member_openid, mute_expire_at)` | 同上 | 单人禁言封装(op=add) | +| `unmute_group_member(group_openid, member_openid)` | 同上 | 单人解禁封装(op=del) | | `prepare_group_upload(group_openid, file_type, file_size, file_name, md5, sha1, md5_10m)` | POST /v2/groups/{group_openid}/upload_prepare | 大文件分片预上传,返回 `upload_id`/`block_size`/各分片预签名 `parts`;逐片 PUT 后调 `finish_group_upload_part`,全部完成后携 `upload_id` 调 `upload_group_file` 合并;`md5_10m` 为文件前约 10MB 的 MD5 | | `finish_group_upload_part(group_openid, upload_id, part_index, block_size=None, md5=None)` | POST …/upload_part_finish | 每个分片 PUT 成功后调用确认 | @@ -42,6 +44,6 @@ | `generate_url_link(callback_data=None)` | 生成机器人分享链接(POST /v2/generate_url_link),返回 `{"data": {"url": …}}`;用户经链接添加时 `callback_data`(≤32 字符)透传给开发者 | | `respond_interaction(interaction_id, code=0)` | 回应互动事件(PUT /interactions/{id});按钮点击收到后必须回应,否则客户端持续 loading,同一 id 仅可回应一次 | -常量:`MSG_TYPE_TEXT=0`、`MSG_TYPE_MARKDOWN=2`、`MSG_TYPE_MEDIA=7`;`FILE_TYPE_IMAGE=1`、`FILE_TYPE_VIDEO=2`、`FILE_TYPE_VOICE=3`、`FILE_TYPE_FILE=4`;流式 `STREAM_INPUT_APPEND`/`STREAM_INPUT_REPLACE`、`STREAM_GENERATING=1`/`STREAM_FINISHED=10`、`STREAM_CONTENT_TEXT`/`STREAM_CONTENT_MARKDOWN`。 +常量:`MSG_TYPE_TEXT=0`、`MSG_TYPE_MARKDOWN=2`、`MSG_TYPE_MEDIA=7`;`FILE_TYPE_IMAGE=1`、`FILE_TYPE_VIDEO=2`、`FILE_TYPE_VOICE=3`、`FILE_TYPE_FILE=4`;流式 `STREAM_INPUT_APPEND`/`STREAM_INPUT_REPLACE`、`STREAM_GENERATING=1`/`STREAM_FINISHED=10`、`STREAM_CONTENT_TEXT`/`STREAM_CONTENT_MARKDOWN`;禁言 `MUTE_OP_ADD`/`MUTE_OP_UPDATE`/`MUTE_OP_DEL`。 按钮构造:`button(label, data, *, type=1, permission=2, style=1)` 生成单个按钮 dict,`keyboard(*rows)` 组装为 `keyboard=` 参数(单按钮独占一行,按钮列表同行并排)。点击回调经 `INTERACTION_CREATE` 事件处理,见[收发消息](/docs/guide/messaging.md#markdown-与按钮msg_type2)。 diff --git a/src/qqbotsdk/api/v2.py b/src/qqbotsdk/api/v2.py index df64f1d..62a4e13 100644 --- a/src/qqbotsdk/api/v2.py +++ b/src/qqbotsdk/api/v2.py @@ -28,6 +28,10 @@ STREAM_GENERATING = 1 # input_state:生成中 STREAM_FINISHED = 10 # input_state:生成结束 STREAM_CONTENT_TEXT = "text" STREAM_CONTENT_MARKDOWN = "markdown" +# 群禁言 members[].op 取值(restrict_chat_setting) +MUTE_OP_ADD = "add" +MUTE_OP_UPDATE = "update" +MUTE_OP_DEL = "del" def button( @@ -384,15 +388,41 @@ class V2Api(BaseApi): """撤回单聊消息(DELETE /v2/users/{openid}/messages/{message_id})。""" return await self._withdraw_v2_message(f"/v2/users/{openid}", message_id) - async def mute_group_member(self, group_openid: str, mute_expire_at: str) -> dict: - """群成员禁言(POST /v2/groups/{group_openid}/restrict_chat_setting)。 + async def mute_group_members( + self, group_openid: str, members: list[dict[str, Any]] + ) -> dict: + """批量设置群成员禁言/解禁(POST .../restrict_chat_setting)。 - mute_expire_at 为禁言到期时间(RFC3339 格式,如 - "2026-01-01T12:00:00+08:00");传当前时间之前即解除禁言。 + members 单次最多 20 项,每项为 {"op": MUTE_OP_ADD/MUTE_OP_UPDATE/ + MUTE_OP_DEL, "member_openid": ..., "mute_expire_at": RFC3339 时间}: + add 新增禁言、update 更新到期时间、del 解除禁言(expire 可传 + 空串)。最长禁言 30 天;仅可操作普通成员(群主/管理员/机器人 + 不可);需群管理员身份,60 QPM。 """ return await self.post( f"/v2/groups/{group_openid}/restrict_chat_setting", - json={"mute_expire_at": mute_expire_at}, + json={"members": members}, + ) + + async def mute_group_member( + self, group_openid: str, member_openid: str, mute_expire_at: str + ) -> dict: + """禁言单个群成员(mute_group_members 的 op=add 封装)。""" + return await self.mute_group_members( + group_openid, + [ + { + "op": MUTE_OP_ADD, + "member_openid": member_openid, + "mute_expire_at": mute_expire_at, + } + ], + ) + + async def unmute_group_member(self, group_openid: str, member_openid: str) -> dict: + """解除单个群成员禁言(mute_group_members 的 op=del 封装)。""" + return await self.mute_group_members( + group_openid, [{"op": MUTE_OP_DEL, "member_openid": member_openid}] ) async def generate_url_link(self, callback_data: str | None = None) -> dict: diff --git a/tests/test_api.py b/tests/test_api.py index 6e39628..4224f82 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -116,10 +116,28 @@ async def test_mute_and_schedule_official_field_names(): assert info["remind_type"] == "0" assert "schedule_info" not in kwargs["json"] - await api.mute_group_member("G1", "2026-01-01T12:00:00+08:00") + await api.mute_group_member("G1", "M1", "2026-01-01T12:00:00+08:00") method, path, kwargs = cap["call"] assert (method, path) == ("POST", "/v2/groups/G1/restrict_chat_setting") - assert kwargs["json"] == {"mute_expire_at": "2026-01-01T12:00:00+08:00"} + assert kwargs["json"] == { + "members": [ + { + "op": "add", + "member_openid": "M1", + "mute_expire_at": "2026-01-01T12:00:00+08:00", + } + ] + } + + await api.unmute_group_member("G1", "M1") + assert cap["call"][2]["json"] == {"members": [{"op": "del", "member_openid": "M1"}]} + + members = [ + {"op": "add", "member_openid": "M1", "mute_expire_at": "t1"}, + {"op": "update", "member_openid": "M2", "mute_expire_at": "t2"}, + ] + await api.mute_group_members("G1", members) + assert cap["call"][2]["json"] == {"members": members} async def test_me():