mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 17:24:48 +00:00
Compare commits
2
Commits
321ab70b35
...
v0.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
706a39b07b | ||
|
|
6235f9e393 |
No files matched your search
@@ -5,6 +5,12 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.1] - 2026-10-03
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- **群成员禁言对齐官方请求结构**:`POST /v2/groups/{group_openid}/restrict_chat_setting` 的请求体实为 `{"members": [{"op": "add"/"update"/"del", "member_openid": …, "mute_expire_at": RFC3339}]}`(单次最多 20 人、最长 30 天、仅可操作普通成员、需群管理员身份、60 QPM),0.2.0 误发顶层 `{"mute_expire_at": …}` 且签名无法指定被禁成员。签名变更:`mute_group_member(group_openid, member_openid, mute_expire_at)`(op=add 单人禁言);新增 `mute_group_members(group_openid, members)` 批量透传与 `unmute_group_member(group_openid, member_openid)`(op=del 解禁,解禁不再走"传过去时间")、`MUTE_OP_ADD`/`MUTE_OP_UPDATE`/`MUTE_OP_DEL` 常量
|
||||||
|
|
||||||
## [0.2.0] - 2026-10-03
|
## [0.2.0] - 2026-10-03
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|||||||
+4
-2
@@ -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` 透传 |
|
| `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` 上传即发送 |
|
| `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 分钟内可撤回 |
|
| `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 |
|
| `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 成功后调用确认 |
|
| `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 字符)透传给开发者 |
|
| `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 仅可回应一次 |
|
| `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)。
|
按钮构造:`button(label, data, *, type=1, permission=2, style=1)` 生成单个按钮 dict,`keyboard(*rows)` 组装为 `keyboard=` 参数(单按钮独占一行,按钮列表同行并排)。点击回调经 `INTERACTION_CREATE` 事件处理,见[收发消息](/docs/guide/messaging.md#markdown-与按钮msg_type2)。
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "qqbotsdk-py"
|
name = "qqbotsdk-py"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
description = "QQ 官方机器人 Python SDK(WebSocket / Webhook)"
|
description = "QQ 官方机器人 Python SDK(WebSocket / Webhook)"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
|
|||||||
+35
-5
@@ -28,6 +28,10 @@ STREAM_GENERATING = 1 # input_state:生成中
|
|||||||
STREAM_FINISHED = 10 # input_state:生成结束
|
STREAM_FINISHED = 10 # input_state:生成结束
|
||||||
STREAM_CONTENT_TEXT = "text"
|
STREAM_CONTENT_TEXT = "text"
|
||||||
STREAM_CONTENT_MARKDOWN = "markdown"
|
STREAM_CONTENT_MARKDOWN = "markdown"
|
||||||
|
# 群禁言 members[].op 取值(restrict_chat_setting)
|
||||||
|
MUTE_OP_ADD = "add"
|
||||||
|
MUTE_OP_UPDATE = "update"
|
||||||
|
MUTE_OP_DEL = "del"
|
||||||
|
|
||||||
|
|
||||||
def button(
|
def button(
|
||||||
@@ -384,15 +388,41 @@ class V2Api(BaseApi):
|
|||||||
"""撤回单聊消息(DELETE /v2/users/{openid}/messages/{message_id})。"""
|
"""撤回单聊消息(DELETE /v2/users/{openid}/messages/{message_id})。"""
|
||||||
return await self._withdraw_v2_message(f"/v2/users/{openid}", 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:
|
async def mute_group_members(
|
||||||
"""群成员禁言(POST /v2/groups/{group_openid}/restrict_chat_setting)。
|
self, group_openid: str, members: list[dict[str, Any]]
|
||||||
|
) -> dict:
|
||||||
|
"""批量设置群成员禁言/解禁(POST .../restrict_chat_setting)。
|
||||||
|
|
||||||
mute_expire_at 为禁言到期时间(RFC3339 格式,如
|
members 单次最多 20 项,每项为 {"op": MUTE_OP_ADD/MUTE_OP_UPDATE/
|
||||||
"2026-01-01T12:00:00+08:00");传当前时间之前即解除禁言。
|
MUTE_OP_DEL, "member_openid": ..., "mute_expire_at": RFC3339 时间}:
|
||||||
|
add 新增禁言、update 更新到期时间、del 解除禁言(expire 可传
|
||||||
|
空串)。最长禁言 30 天;仅可操作普通成员(群主/管理员/机器人
|
||||||
|
不可);需群管理员身份,60 QPM。
|
||||||
"""
|
"""
|
||||||
return await self.post(
|
return await self.post(
|
||||||
f"/v2/groups/{group_openid}/restrict_chat_setting",
|
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:
|
async def generate_url_link(self, callback_data: str | None = None) -> dict:
|
||||||
|
|||||||
+20
-2
@@ -116,10 +116,28 @@ async def test_mute_and_schedule_official_field_names():
|
|||||||
assert info["remind_type"] == "0"
|
assert info["remind_type"] == "0"
|
||||||
assert "schedule_info" not in kwargs["json"]
|
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"]
|
method, path, kwargs = cap["call"]
|
||||||
assert (method, path) == ("POST", "/v2/groups/G1/restrict_chat_setting")
|
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():
|
async def test_me():
|
||||||
|
|||||||
@@ -688,7 +688,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "qqbotsdk-py"
|
name = "qqbotsdk-py"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
|||||||
Reference in new issue
Block a user