mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 13:55:20 +00:00
feat: OpenAPI 全量对齐——对照新旧两版官方文档补齐全部接口与事件
- 新增 panel 域:指令面板(斜线指令)增删改查/关联对象 + /v2/menu 全局自定义菜单 - 新增 group 域:群信息/成员/机器人群内状态、入群申请列表与审批、群黑名单、 群禁言状态、入群自动审批策略(增删改查/执行/白名单) - v2 补充:流式单聊消息、群/单聊大文件分片上传(预上传+分片确认)、分享链接 - guild 补充:子频道增删改、机器人频道列表、身份组 CRUD 与成员授予/收回、 成员列表/踢出、全员与批量禁言、接口权限、消息频率设置 - channel 补充:子频道用户/身份组权限、频道私信(建会话/发信/撤回) - 新增 audio 域(播放控制、上/下麦)与 forum 域(帖子增删查) - 事件补齐 GROUP_MEMBER_ADD/REMOVE、GROUP_JOIN_REQUEST、SUBSCRIBE_MESSAGE_STATUS; Intent 与 intents.toml 补 GROUP_MEMBER_EVENT (1<<24) 分组(默认 false)
This commit is contained in:
1 parent
13a1d971be
commit
15e1f5e303
15 files changed
+1677
-16
No files matched your search
@@ -7,6 +7,12 @@
|
|||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|
||||||
|
- **旧版(频道侧)OpenAPI 对齐(对照旧版 wiki 全集核对)**:私信(`create_dms` 建会话、`post_dms_message` 发信、`withdraw_dms_message` 撤回);身份组增删改与成员授予/收回;频道成员列表/踢出(可拉黑+撤回历史消息)/身份组成员列表/子频道在线成员数;全员与批量成员禁言;子频道用户/身份组权限查改;频道消息频率设置;接口权限列表与授权链接申请;新增 `audio` 域(播放控制、上/下麦)与 `forum` 域(帖子列表/详情/发帖/删帖)组合进 `BotApi`(`AUDIO_STATUS_*`、`FORUM_FORMAT_*` 常量随模块导出)
|
||||||
|
- **事件全量对齐**:新增 `GROUP_MEMBER_ADD`/`GROUP_MEMBER_REMOVE`(共用 `GroupMemberChange`)、`GROUP_JOIN_REQUEST`(入群申请,含验证问答与自动审批命中信息,仅机器人是群管理员时推送)、`SUBSCRIBE_MESSAGE_STATUS`(订阅消息授权状态)四个 payload;`Intent` 枚举与 intents.toml 补上此前缺失的 `GROUP_MEMBER_EVENT (1<<24)` 分组(默认 false,需订阅请手动开启)
|
||||||
|
- **v2 OpenAPI 全量对齐(对照官方 2026-09 新版文档站逐页核对)**:新增 `group` 域方法集组合进 `BotApi`——群信息/成员列表/成员详情/机器人群内状态、入群申请列表与审批、群黑名单增删查、群成员批量移除、群禁言状态查询、入群自动审批策略(增删改查/执行/白名单,机器人维度全局配置)
|
||||||
|
- v2 消息扩展:`post_c2c_stream_message` 流式分批发送单聊消息(AI 逐段下发,`STREAM_*` 常量取值);`prepare_group_upload`/`finish_group_upload_part` 与 `prepare_c2c_upload`/`finish_c2c_upload_part` 大文件分片上传(预上传拿预签名 URL,分片 PUT 后逐片确认,最后携 `upload_id` 走原上传接口合并);`generate_url_link` 生成机器人分享链接(`callback_data` 透传)
|
||||||
|
- 频道域补充:`create_channel`/`update_channel`/`delete_channel` 子频道增删改(私域接口)、`get_me_guilds` 机器人加入的频道列表(分页)
|
||||||
|
- **斜线指令管理**:新增 `panel` 域方法集组合进 `BotApi`——指令面板(聊天输入框输入 `/` 唤起的指令列表)的增删改查与关联对象管理(`create_panel`/`get_panels`/`get_panel`/`update_panel`/`delete_panel`/`update_panel_targets`,支持 c2c/group/channel/dm 四场景,c2c/group 可按指定对象生效);附全局自定义菜单 `get_menu`/`update_menu`(仅 C2C,`send_message` 类型点击后把 `/指令` 填入输入框);`SCOPE_*`/`TARGET_*`/`ITEM_*` 等取值常量随模块导出
|
||||||
- Markdown 与内嵌按钮一等支持:`post_group_message`/`post_c2c_message` 新增 `markdown`/`keyboard` 参数(自动置 msg_type=2,与 content 互斥);新增 `respond_interaction()` 回应按钮回调;`Interaction` payload 补齐 `chat_type`/`scene`/`group_openid` 等官方字段
|
- Markdown 与内嵌按钮一等支持:`post_group_message`/`post_c2c_message` 新增 `markdown`/`keyboard` 参数(自动置 msg_type=2,与 content 互斥);新增 `respond_interaction()` 回应按钮回调;`Interaction` payload 补齐 `chat_type`/`scene`/`group_openid` 等官方字段
|
||||||
- `Event` 新增 `event_id` 属性(推送信封顶层的事件 id,与 op/t/d 平级):非消息事件(GROUP_ADD_ROBOT 等)的被动回复不再需要手动 `event.raw["id"]`
|
- `Event` 新增 `event_id` 属性(推送信封顶层的事件 id,与 op/t/d 平级):非消息事件(GROUP_ADD_ROBOT 等)的被动回复不再需要手动 `event.raw["id"]`
|
||||||
- 按钮快捷构造与点击处理:`button()`/`keyboard()` 生成内嵌按钮(免手写嵌套 dict);`Interaction` 新增 `button_data` 属性(被点按钮的 action.data);examples/group_bot 补 `/btn` 卡片与 `INTERACTION_CREATE` 回调示例
|
- 按钮快捷构造与点击处理:`button()`/`keyboard()` 生成内嵌按钮(免手写嵌套 dict);`Interaction` 新增 `button_data` 属性(被点按钮的 action.data);examples/group_bot 补 `/btn` 卡片与 `INTERACTION_CREATE` 回调示例
|
||||||
|
|||||||
+86
-2
@@ -97,7 +97,7 @@ async def slow(...): ...
|
|||||||
|
|
||||||
`from qqbotsdk.api import BotApi`。经参数注入(`api: BotApi`)。自动附带 `Authorization: QQBot <token>` 鉴权头并刷新 token;HTTP ≥400 抛 `ApiError`。
|
`from qqbotsdk.api import BotApi`。经参数注入(`api: BotApi`)。自动附带 `Authorization: QQBot <token>` 鉴权头并刷新 token;HTTP ≥400 抛 `ApiError`。
|
||||||
|
|
||||||
实现按域拆在 `qqbotsdk/api/` 包内(`core` 请求内核 + `v2`/`channel`/`guild` 方法集组合成 `BotApi`),方法名与下表一致;429 限流自动重试(最多 3 次,优先 `Retry-After`,退避封顶 30s),401 强制刷新 token 后重试一次。
|
实现按域拆在 `qqbotsdk/api/` 包内(`core` 请求内核 + `v2`/`channel`/`guild`/`panel`/`group`/`audio`/`forum` 方法集组合成 `BotApi`),方法名与下表一致;429 限流自动重试(最多 3 次,优先 `Retry-After`,退避封顶 30s),401 强制刷新 token 后重试一次。
|
||||||
|
|
||||||
### 通用
|
### 通用
|
||||||
|
|
||||||
@@ -106,9 +106,10 @@ async def slow(...): ...
|
|||||||
| `request(method, path, **kwargs)` | 通用请求,kwargs 透传 aiohttp(`json=`/`params=` 等) |
|
| `request(method, path, **kwargs)` | 通用请求,kwargs 透传 aiohttp(`json=`/`params=` 等) |
|
||||||
| `get` / `post` / `delete` / `put` / `patch`(path, **kwargs) | `request` 快捷方式 |
|
| `get` / `post` / `delete` / `put` / `patch`(path, **kwargs) | `request` 快捷方式 |
|
||||||
| `me()` | 机器人自身信息(GET /users/@me) |
|
| `me()` | 机器人自身信息(GET /users/@me) |
|
||||||
|
| `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});按钮点击(INTERACTION_CREATE type=11)收到后必须回应,否则客户端持续 loading,同一 id 仅可回应一次 |
|
| `respond_interaction(interaction_id, code=0)` | 回应互动事件(PUT /interactions/{id});按钮点击(INTERACTION_CREATE type=11)收到后必须回应,否则客户端持续 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`。
|
常量:`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`;音频 `AUDIO_STATUS_START=0`/`PAUSE=1`/`RESUME=2`/`STOP=3`;论坛 `FORUM_FORMAT_TEXT=1`/`HTML=2`/`MARKDOWN=3`/`JSON=4`。
|
||||||
|
|
||||||
按钮构造:`button(label, data, *, type=1, permission=2, style=1)` 生成单个按钮 dict,`keyboard(*rows)` 组装为 `keyboard=` 参数(单按钮独占一行,按钮列表同行并排)。点击回调经 `INTERACTION_CREATE` 事件处理:`Interaction.button_data` 取被点按钮的 `data`,`Interaction.id` 用于 `respond_interaction`,**被动回复的 `event_id` 用 `Event.event_id`(网关帧最外层 id),不能用 `Interaction.id`**。
|
按钮构造:`button(label, data, *, type=1, permission=2, style=1)` 生成单个按钮 dict,`keyboard(*rows)` 组装为 `keyboard=` 参数(单按钮独占一行,按钮列表同行并排)。点击回调经 `INTERACTION_CREATE` 事件处理:`Interaction.button_data` 取被点按钮的 `data`,`Interaction.id` 用于 `respond_interaction`,**被动回复的 `event_id` 用 `Event.event_id`(网关帧最外层 id),不能用 `Interaction.id`**。
|
||||||
|
|
||||||
@@ -120,6 +121,8 @@ async def slow(...): ...
|
|||||||
| `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_member(group_openid, mute_expire_at)` | POST /v2/groups/{group_openid}/restrict_chat_setting | `mute_expire_at` 为 RFC3339 到期时间;传早于当前的时间即解禁 |
|
||||||
|
| `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 成功后调用确认 |
|
||||||
|
|
||||||
### v2 单聊(用户 openid)
|
### v2 单聊(用户 openid)
|
||||||
|
|
||||||
@@ -128,6 +131,31 @@ async def slow(...): ...
|
|||||||
| `post_c2c_message(openid, content="", msg_id=None, msg_seq=1, media=None, event_id=None, markdown=None, keyboard=None, **extra)` | POST /v2/users/{openid}/messages | 同 `post_group_message` |
|
| `post_c2c_message(openid, content="", msg_id=None, msg_seq=1, media=None, event_id=None, markdown=None, keyboard=None, **extra)` | POST /v2/users/{openid}/messages | 同 `post_group_message` |
|
||||||
| `upload_c2c_file(openid, file_type, url, srv_send_msg=False)` | POST /v2/users/{openid}/files | 同 `upload_group_file` |
|
| `upload_c2c_file(openid, file_type, url, srv_send_msg=False)` | POST /v2/users/{openid}/files | 同 `upload_group_file` |
|
||||||
| `withdraw_c2c_message(openid, message_id)` | DELETE /v2/users/{openid}/messages/{message_id} | 2 分钟内可撤回 |
|
| `withdraw_c2c_message(openid, message_id)` | DELETE /v2/users/{openid}/messages/{message_id} | 2 分钟内可撤回 |
|
||||||
|
| `post_c2c_stream_message(openid, content_raw="", *, stream_msg_id=None, index=0, input_state=STREAM_GENERATING, input_mode=None, content_type=STREAM_CONTENT_MARKDOWN, msg_id=None, event_id=None, msg_seq=None, is_wakeup=None)` | POST /v2/users/{openid}/stream_messages | 流式分批发送(AI 回复逐段下发):首片不传 `stream_msg_id`,响应 `id` 即后续分片要携带的值,`index` 从 0 递增,以 `input_state=STREAM_FINISHED` 收尾;`input_mode="replace"` 时 `content_raw` 须以上游已下发前缀开头;`is_wakeup=True` 为召回消息,不校验被动回复有效期 |
|
||||||
|
| `prepare_c2c_upload(openid, …)` / `finish_c2c_upload_part(openid, …)` | POST /v2/users/{openid}/upload_prepare · upload_part_finish | 单聊分片上传,用法同群聊 `prepare_group_upload`/`finish_group_upload_part` |
|
||||||
|
|
||||||
|
### 群管理(群 openid)
|
||||||
|
|
||||||
|
多数接口要求机器人拥有群管理员身份;成员/黑名单/批量移除等能力官方标注"内邀接入中",未开通时服务端返回业务码 11253。常量:`APPROVE`/`DECLINE`(审批动作)、`OP_ADD`/`OP_DEL`(黑名单/白名单操作)。
|
||||||
|
|
||||||
|
| 方法 | 端点 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `get_group_info(group_openid)` | GET /v2/groups/{group_openid}/info | 群名称/简介/分类/标签/成员数 |
|
||||||
|
| `get_group_members(group_openid, cursor=None)` | GET …/members | 成员列表,每页最多 30 条,`next_cursor` 翻页(空串=末页) |
|
||||||
|
| `get_group_member(group_openid, member_openid)` | GET …/members/{member_openid} | 成员昵称/角色(member/owner/admin)/入群时间 |
|
||||||
|
| `get_group_bot_state(group_openid)` | GET …/bot_state | 机器人自身状态:入群时间、是否允许主动推送、群内消息接收设置、角色 |
|
||||||
|
| `remove_group_members(group_openid, member_openids, add_to_member_blacklist=False)` | POST …/batch_remove_members | 批量移除,单次最多 20 个,可选拉黑 |
|
||||||
|
| `get_group_join_requests(group_openid, cursor=None, limit=None)` | GET …/join_request_list | 入群申请列表(含验证问答),需群管理员;limit 默认 20 最大 50 |
|
||||||
|
| `review_group_join_request(group_openid, member_openid, op, join_request_id=None, reject_reason=None, add_to_member_blacklist=None)` | POST …/approval_join_request/{member_openid} | `op`:`APPROVE`/`DECLINE`;拒绝时可附理由并拉黑 |
|
||||||
|
| `get_group_mute_state(group_openid)` | GET …/restrict_chat_setting | 禁言状态:全员禁言规则(定时/周期)与被禁言成员列表 |
|
||||||
|
| `get_group_blacklist(group_openid, cursor=None, limit=None)` | GET …/member_blacklist | 黑名单列表,limit 默认 20 最大 100 |
|
||||||
|
| `update_group_blacklist(group_openid, op, member_openids)` | POST …/member_blacklist | `op`:`OP_ADD`/`OP_DEL`,单次最多 20 个;成员在群中时无法拉黑 |
|
||||||
|
| `get_join_approval_strategies(cursor=None, limit=None)` | GET /v2/groups/join_approval_strategy | 自动审批策略列表(机器人维度全局配置),按创建时间倒序 |
|
||||||
|
| `create_join_approval_strategy(*, group_openids=None, group_ids=None, is_enable=None, expire_at=None, remark=None)` | POST /v2/groups/join_approval_strategy | 关联群两种标识二选一(互斥,最多 100 个);`is_enable` "on"/"off";默认一年过期;仅机器人有群管理员身份时生效;返回 `strategy_id` |
|
||||||
|
| `update_join_approval_strategy(strategy_id, *, is_enable=None, expire_at=None, group_action=None, remark=None)` | PATCH …/{strategy_id} | 只传变更项;`group_action` 为 `{"op": "add"/"del", "group_openids": […]}`,群标识形式须与创建时一致 |
|
||||||
|
| `delete_join_approval_strategy(strategy_id)` | DELETE …/{strategy_id} | 删除策略 |
|
||||||
|
| `execute_join_approval_strategy(strategy_id)` | POST …/{strategy_id}/execute | 全量扫描关联群并自动通过白名单申请;异步约 10 分钟 |
|
||||||
|
| `update_join_approval_strategy_whitelist(strategy_id, op, whitelist_users)` | POST …/{strategy_id}/whitelist_users | QQ 号码字符串列表,单次最多 10000、上限 10 万 |
|
||||||
|
|
||||||
### 频道消息(channel_id / guild_id)
|
### 频道消息(channel_id / guild_id)
|
||||||
|
|
||||||
@@ -137,12 +165,20 @@ async def slow(...): ...
|
|||||||
| `withdraw_channel_message(channel_id, message_id, hide_tip=False)` | DELETE /channels/{channel_id}/messages/{message_id} | **仅私域机器人**;`hide_tip` 对应官方 `hidetip` |
|
| `withdraw_channel_message(channel_id, message_id, hide_tip=False)` | DELETE /channels/{channel_id}/messages/{message_id} | **仅私域机器人**;`hide_tip` 对应官方 `hidetip` |
|
||||||
| `put_channel_reaction(channel_id, message_id, emoji_type, emoji_id)` | PUT …/reactions/{type}/{emoji_id} | `emoji_type=1` 系统表情,`emoji_id` 为序号字符串 |
|
| `put_channel_reaction(channel_id, message_id, emoji_type, emoji_id)` | PUT …/reactions/{type}/{emoji_id} | `emoji_type=1` 系统表情,`emoji_id` 为序号字符串 |
|
||||||
| `delete_channel_reaction(channel_id, message_id, emoji_type, emoji_id)` | DELETE …/reactions/{type}/{emoji_id} | 删除自己的表态 |
|
| `delete_channel_reaction(channel_id, message_id, emoji_type, emoji_id)` | DELETE …/reactions/{type}/{emoji_id} | 删除自己的表态 |
|
||||||
|
| `create_dms(recipient_id, source_guild_id)` | POST /users/@me/dms | 创建私信会话(需与用户同频道),返回私信的 `guild_id`/`channel_id` |
|
||||||
|
| `post_dms_message(guild_id, content="", msg_id=None, msg_seq=1, image=None, **extra)` | POST /dms/{guild_id}/messages | 发私信,参数同 `post_channel_message`;主动消息每天单用户 2 条、累计 200 条,被动不限 |
|
||||||
|
| `withdraw_dms_message(guild_id, message_id, hide_tip=False)` | DELETE /dms/{guild_id}/messages/{message_id} | 撤回自己发的私信,**仅私域** |
|
||||||
|
| `get_channel_permissions(channel_id, user_id)` / `get_channel_role_permissions(channel_id, role_id)` | GET …/members/{user_id}/permissions · …/roles/{role_id}/permissions | 子频道权限位图(字符串) |
|
||||||
|
| `update_channel_permissions(channel_id, user_id, *, add=None, remove=None)` / `update_channel_role_permissions(channel_id, role_id, …)` | PUT 同路径 | `add`/`remove` 为字符串权限位图,至少传一个,需管理员权限 |
|
||||||
|
|
||||||
### 频道管理
|
### 频道管理
|
||||||
|
|
||||||
| 方法 | 端点 | 说明 |
|
| 方法 | 端点 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `create_guild_announce(guild_id, channel_id, message_id, announces_type=0, **extra)` | POST /guilds/{guild_id}/announces | `announces_type`:0 成员公告 / 1 欢迎公告;推荐频道公告经 extra(与消息公告互斥) |
|
| `create_guild_announce(guild_id, channel_id, message_id, announces_type=0, **extra)` | POST /guilds/{guild_id}/announces | `announces_type`:0 成员公告 / 1 欢迎公告;推荐频道公告经 extra(与消息公告互斥) |
|
||||||
|
| `create_channel(guild_id, name, **fields)` | POST /guilds/{guild_id}/channels | 创建子频道,**私域**需管理员权限;`fields` 透传 type/sub_type/position/parent_id/private_type/speak_permission 等;触发 CHANNEL_CREATE |
|
||||||
|
| `update_channel(channel_id, **fields)` | PATCH /channels/{channel_id} | 修改子频道,**私域**;只传变更项;触发 CHANNEL_UPDATE |
|
||||||
|
| `delete_channel(channel_id)` | DELETE /channels/{channel_id} | 删除子频道,**私域**;不可恢复;触发 CHANNEL_DELETE |
|
||||||
| `delete_guild_announce(guild_id, message_id)` | DELETE /guilds/{guild_id}/announces/{message_id} | 推荐频道公告传 `message_id="all"` |
|
| `delete_guild_announce(guild_id, message_id)` | DELETE /guilds/{guild_id}/announces/{message_id} | 推荐频道公告传 `message_id="all"` |
|
||||||
| `pin_channel_message(channel_id, message_id)` | PUT /channels/{channel_id}/pins/{message_id} | 添加精华消息 |
|
| `pin_channel_message(channel_id, message_id)` | PUT /channels/{channel_id}/pins/{message_id} | 添加精华消息 |
|
||||||
| `unpin_channel_message(channel_id, message_id)` | DELETE /channels/{channel_id}/pins/{message_id} | 移出精华消息 |
|
| `unpin_channel_message(channel_id, message_id)` | DELETE /channels/{channel_id}/pins/{message_id} | 移出精华消息 |
|
||||||
@@ -152,6 +188,14 @@ async def slow(...): ...
|
|||||||
| `create_channel_schedule(channel_id, name, start_timestamp, end_timestamp, jump_channel_id, remind_type="0", description="")` | POST /channels/{channel_id}/schedules | 时间为毫秒时间戳字符串;`remind_type` "0" 不提醒 / "1" 开始时;请求体自动包在 `schedule` 字段;单管理员每天限 10 次 |
|
| `create_channel_schedule(channel_id, name, start_timestamp, end_timestamp, jump_channel_id, remind_type="0", description="")` | POST /channels/{channel_id}/schedules | 时间为毫秒时间戳字符串;`remind_type` "0" 不提醒 / "1" 开始时;请求体自动包在 `schedule` 字段;单管理员每天限 10 次 |
|
||||||
| `update_channel_schedule(channel_id, schedule_id, **fields)` | PATCH /channels/{channel_id}/schedules/{schedule_id} | `fields` 为 schedule 内字段透传 |
|
| `update_channel_schedule(channel_id, schedule_id, **fields)` | PATCH /channels/{channel_id}/schedules/{schedule_id} | `fields` 为 schedule 内字段透传 |
|
||||||
| `delete_channel_schedule(channel_id, schedule_id)` | DELETE /channels/{channel_id}/schedules/{schedule_id} | 删除日程 |
|
| `delete_channel_schedule(channel_id, schedule_id)` | DELETE /channels/{channel_id}/schedules/{schedule_id} | 删除日程 |
|
||||||
|
| `create_guild_role(guild_id, name="", color=None, hoist=None)` | POST /guilds/{guild_id}/roles | 创建身份组,返回 `{"role_id", "role"}`;`color` 为 ARGB 十进制 |
|
||||||
|
| `update_guild_role(guild_id, role_id, name=None, color=None, hoist=None)` | PATCH /guilds/{guild_id}/roles/{role_id} | 只传变更项;系统默认身份组不可改 |
|
||||||
|
| `delete_guild_role(guild_id, role_id)` | DELETE /guilds/{guild_id}/roles/{role_id} | 只能删除自建身份组 |
|
||||||
|
| `add_guild_member_role(guild_id, user_id, role_id)` / `remove_guild_member_role(…)` | PUT / DELETE /guilds/{guild_id}/members/{user_id}/roles/{role_id} | 授予/收回成员身份组 |
|
||||||
|
| `remove_guild_member(guild_id, user_id, add_blacklist=False, delete_history_msg_days=0)` | DELETE /guilds/{guild_id}/members/{user_id} | 踢人,**私域**需踢人权限;撤回消息仅支持 3/7/15/30 或 -1 全部 |
|
||||||
|
| `mute_guild(guild_id, mute_end_timestamp=None, mute_seconds=None)` | PATCH /guilds/{guild_id}/mute | 全员禁言,两个时间字段二选一;解除都传 "0" |
|
||||||
|
| `mute_guild_members(guild_id, user_ids, …)` | PATCH /guilds/{guild_id}/mute | 批量禁言(带 `user_ids`),单次最多 20 人 |
|
||||||
|
| `create_api_permission_demand(guild_id, channel_id, path, method, desc)` | POST /guilds/{guild_id}/api_permission/demand | 生成接口权限授权链接发到子频道,由频道管理员授权 |
|
||||||
| `mute_guild_member(guild_id, user_id, mute_seconds=0)` | PATCH /guilds/{guild_id}/members/{user_id}/mute | 禁言秒数(上限 28 天),0 解禁 |
|
| `mute_guild_member(guild_id, user_id, mute_seconds=0)` | PATCH /guilds/{guild_id}/members/{user_id}/mute | 禁言秒数(上限 28 天),0 解禁 |
|
||||||
|
|
||||||
### 频道查询
|
### 频道查询
|
||||||
@@ -159,11 +203,45 @@ async def slow(...): ...
|
|||||||
| 方法 | 端点 |
|
| 方法 | 端点 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `get_guild(guild_id)` | GET /guilds/{guild_id} |
|
| `get_guild(guild_id)` | GET /guilds/{guild_id} |
|
||||||
|
| `get_me_guilds(before=None, after=None, limit=None)` | GET /users/@me/guilds |
|
||||||
| `get_guild_channels(guild_id)` | GET /channels/{guild_id}/channels |
|
| `get_guild_channels(guild_id)` | GET /channels/{guild_id}/channels |
|
||||||
|
| `get_guild_members(guild_id)` | GET /guilds/{guild_id}/members |
|
||||||
|
| `get_role_members(guild_id, role_id)` | GET /guilds/{guild_id}/roles/{role_id}/members |
|
||||||
|
| `get_channel_online_nums(channel_id)` | GET /channels/{channel_id}/online_nums |
|
||||||
|
| `get_guild_message_setting(guild_id)` | GET /guilds/{guild_id}/message/setting |
|
||||||
|
| `get_guild_api_permissions(guild_id)` | GET /guilds/{guild_id}/api_permission |
|
||||||
| `get_channel(channel_id)` | GET /channels/{channel_id} |
|
| `get_channel(channel_id)` | GET /channels/{channel_id} |
|
||||||
| `get_guild_roles(guild_id)` | GET /guilds/{guild_id}/roles |
|
| `get_guild_roles(guild_id)` | GET /guilds/{guild_id}/roles |
|
||||||
| `get_guild_member(guild_id, user_id)` | GET /guilds/{guild_id}/members/{user_id} |
|
| `get_guild_member(guild_id, user_id)` | GET /guilds/{guild_id}/members/{user_id} |
|
||||||
|
|
||||||
|
### 指令面板(斜线指令)与全局菜单
|
||||||
|
|
||||||
|
斜线指令面板即用户在聊天输入框输入 `/` 唤起的指令列表,支持 c2c(单聊)、group(群聊)、channel(文字子频道)、dm(频道私信)四种场景;c2c/group 支持 `target_type="specific"` 按指定对象生效(关联对象每次最多 20 个,详情最多查 1000 个),channel/dm 仅全局生效。一个机器人最多 20 个面板,每面板最多 20 个元素(`name` ≤14 字符、`desc` ≤30 字符)。常量:`SCOPE_C2C`/`SCOPE_GROUP`/`SCOPE_CHANNEL`/`SCOPE_DM`、`TARGET_ALL`/`TARGET_SPECIFIC`、`ITEM_COMMAND`/`ITEM_LINK`、`TARGET_OP_ADD`/`TARGET_OP_DEL`。
|
||||||
|
|
||||||
|
| 方法 | 端点 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `create_panel(scope, panel, *, target_type=None, user_openids=None, group_openids=None, **extra)` | POST /v2/panels | 返回 `{"panel_id": …}`;`panel` 为 `{"items": [{"type": "command"/"link", "name", "desc", "only_admin", "link"}], "remark"}`,command 类型点击后 `name` 填入输入框,link 类型点击跳转 |
|
||||||
|
| `get_panels(scope, cursor=None, limit=None)` | GET /v2/panels | 分页列表(按设置时间倒序),返回 `records`/`next_cursor`(空串=末页)/`is_end`;`limit` 默认 20、最大 50 |
|
||||||
|
| `get_panel(panel_id)` | GET /v2/panels/{panel_id} | 面板详情,specific 面板另带关联的 `user_openids`/`group_openids` |
|
||||||
|
| `update_panel(panel_id, panel)` | PUT /v2/panels/{panel_id} | 整体覆盖元素与备注,不影响已关联对象;返回 `{"version": …}` |
|
||||||
|
| `delete_panel(panel_id)` | DELETE /v2/panels/{panel_id} | 删除面板,删除后对所有对象失效 |
|
||||||
|
| `update_panel_targets(panel_id, op, user_openids=None, group_openids=None)` | PUT /v2/panels/{panel_id}/target | `op`:`"add"`/`"del"`;`user_openids` 仅 c2c、`group_openids` 仅 group;channel/dm 与全局面板不支持 |
|
||||||
|
| `get_menu()` | GET /v2/menu | 查询全局自定义菜单(仅 C2C、对所有用户生效),返回 `{"version", "menu"}`;未设置过时 `menu` 为空 |
|
||||||
|
| `update_menu(menu=None)` | PUT /v2/menu | 整体覆盖菜单(≤10 项,子菜单 ≤5 且不可再嵌套);`send_message` 类型点击后把文本(如 `/help`)填入输入框,常与斜线指令配合 |
|
||||||
|
|
||||||
|
### 音频与论坛(私域)
|
||||||
|
|
||||||
|
音频接口仅音频类机器人可用(需联系平台开通);论坛仅论坛子频道,发帖/删帖为异步任务(返回 `task_id`,结果经 FORUM_* 事件推送)。
|
||||||
|
|
||||||
|
| 方法 | 端点 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `control_channel_audio(channel_id, audio_url=None, text=None, status=AUDIO_STATUS_START)` | POST /channels/{channel_id}/audio | 播放控制(START/PAUSE/RESUME/STOP);`audio_url`/`text` 仅 START 时传 |
|
||||||
|
| `put_channel_mic(channel_id)` / `delete_channel_mic(channel_id)` | PUT / DELETE /channels/{channel_id}/mic | 机器人上麦 / 下麦 |
|
||||||
|
| `get_channel_threads(channel_id)` | GET /channels/{channel_id}/threads | 帖子列表 `{"threads", "is_finish"}` |
|
||||||
|
| `get_channel_thread(channel_id, thread_id)` | GET …/threads/{thread_id} | 帖子详情 |
|
||||||
|
| `put_channel_thread(channel_id, title, content, format=FORUM_FORMAT_TEXT)` | PUT /channels/{channel_id}/threads | 发帖,`format` 为 FORUM_FORMAT_* 常量;返回 `task_id` |
|
||||||
|
| `delete_channel_thread(channel_id, thread_id)` | DELETE …/threads/{thread_id} | 删帖,返回 `task_id` |
|
||||||
|
|
||||||
### ApiError
|
### ApiError
|
||||||
|
|
||||||
`ApiError(RuntimeError)`,重试后仍失败(HTTP ≥400)时抛出:
|
`ApiError(RuntimeError)`,重试后仍失败(HTTP ≥400)时抛出:
|
||||||
@@ -192,6 +270,9 @@ async def slow(...): ...
|
|||||||
| | `GROUP_ADD_ROBOT` / `GROUP_DEL_ROBOT` | `GroupAddRobot` / `GroupDelRobot` |
|
| | `GROUP_ADD_ROBOT` / `GROUP_DEL_ROBOT` | `GroupAddRobot` / `GroupDelRobot` |
|
||||||
| | `C2C_MSG_REJECT` / `C2C_MSG_RECEIVE` | `C2CMsgReject` / `C2CMsgReceive` |
|
| | `C2C_MSG_REJECT` / `C2C_MSG_RECEIVE` | `C2CMsgReject` / `C2CMsgReceive` |
|
||||||
| | `GROUP_MSG_REJECT` / `GROUP_MSG_RECEIVE` | `GroupMsgReject` / `GroupMsgReceive` |
|
| | `GROUP_MSG_REJECT` / `GROUP_MSG_RECEIVE` | `GroupMsgReject` / `GroupMsgReceive` |
|
||||||
|
| | `SUBSCRIBE_MESSAGE_STATUS` | `SubscribeMessageStatus` |
|
||||||
|
| GROUP_MEMBER_EVENT | `GROUP_MEMBER_ADD` / `GROUP_MEMBER_REMOVE` | `GroupMemberChange` |
|
||||||
|
| | `GROUP_JOIN_REQUEST`(仅机器人是群管理员时推送) | `GroupJoinRequest` |
|
||||||
| PUBLIC_GUILD_MESSAGES | `AT_MESSAGE_CREATE` | `AtMessage` |
|
| PUBLIC_GUILD_MESSAGES | `AT_MESSAGE_CREATE` | `AtMessage` |
|
||||||
| | `PUBLIC_MESSAGE_DELETE` | `MessageDelete` |
|
| | `PUBLIC_MESSAGE_DELETE` | `MessageDelete` |
|
||||||
| GUILD_MESSAGES(私域) | `MESSAGE_DELETE` | `MessageDelete` |
|
| GUILD_MESSAGES(私域) | `MESSAGE_DELETE` | `MessageDelete` |
|
||||||
@@ -220,6 +301,9 @@ GroupAtMessage # id, content, group_openid, author, timestamp,
|
|||||||
C2CMessage # id, content, author, attachments, timestamp
|
C2CMessage # id, content, author, attachments, timestamp
|
||||||
AtMessage # id, content, channel_id, guild_id, author, member, timestamp
|
AtMessage # id, content, channel_id, guild_id, author, member, timestamp
|
||||||
DirectMessage # 同 AtMessage
|
DirectMessage # 同 AtMessage
|
||||||
|
GroupJoinRequest # group_openid, join_request_id, member_openid, username, risk_tips,
|
||||||
|
# apply_at, apply_source, invited_by, verify_info, auto_approved, …
|
||||||
|
GroupMemberChange # timestamp, group_openid, member_openid, user_openid
|
||||||
```
|
```
|
||||||
|
|
||||||
- `GroupAtMessage.user_openid` / `C2CMessage.user_openid`:便利属性,从 `author.member_openid`/`author.user_openid` 取发言人标识。
|
- `GroupAtMessage.user_openid` / `C2CMessage.user_openid`:便利属性,从 `author.member_openid`/`author.user_openid` 取发言人标识。
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,8 @@ GUILD_MEMBERS = false # (1 << 1) 频道成员增删改
|
|||||||
GUILD_MESSAGES = false # (1 << 9) 频道全量消息,仅 *私域* 机器人
|
GUILD_MESSAGES = false # (1 << 9) 频道全量消息,仅 *私域* 机器人
|
||||||
GUILD_MESSAGE_REACTIONS = false # (1 << 10) 频道表情表态
|
GUILD_MESSAGE_REACTIONS = false # (1 << 10) 频道表情表态
|
||||||
DIRECT_MESSAGE = false # (1 << 12) 频道私信
|
DIRECT_MESSAGE = false # (1 << 12) 频道私信
|
||||||
GROUP_AND_C2C_EVENT = true # (1 << 25) 群/单聊消息、关注与主动消息开关事件
|
GROUP_MEMBER_EVENT = false # (1 << 24) 群成员加入/退出、用户申请加群(后者仅机器人是群管理员时推送)
|
||||||
|
GROUP_AND_C2C_EVENT = true # (1 << 25) 群/单聊消息、关注与主动消息开关、订阅消息授权状态事件
|
||||||
INTERACTION = true # (1 << 26) 互动事件(卡片按钮回调)
|
INTERACTION = true # (1 << 26) 互动事件(卡片按钮回调)
|
||||||
MESSAGE_AUDIT = false # (1 << 27) 消息审核结果
|
MESSAGE_AUDIT = false # (1 << 27) 消息审核结果
|
||||||
FORUMS_EVENT = false # (1 << 28) 论坛事件,仅 *私域* 机器人
|
FORUMS_EVENT = false # (1 << 28) 论坛事件,仅 *私域* 机器人
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
"""OpenAPI 客户端:BotApi 按域组合——v2 群/单聊、频道消息、频道管理。
|
"""OpenAPI 客户端:BotApi 按域组合——v2 群/单聊、频道消息、频道管理、
|
||||||
|
指令面板(斜线指令)与全局菜单、群管理、音频、论坛。
|
||||||
|
|
||||||
各域模块是方法集(继承 BaseApi 共享请求内核:鉴权头注入、429 自动
|
各域模块是方法集(继承 BaseApi 共享请求内核:鉴权头注入、429 自动
|
||||||
重试、401 刷新 token 重试、统一 ApiError),方法名与拆分前完全一致。
|
重试、401 刷新 token 重试、统一 ApiError),方法名与拆分前完全一致。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .audio import AudioApi
|
||||||
from .channel import ChannelApi
|
from .channel import ChannelApi
|
||||||
from .core import ApiError
|
from .core import ApiError
|
||||||
from .core import BaseApi as BaseApi
|
from .core import BaseApi as BaseApi
|
||||||
|
from .forum import ForumApi
|
||||||
|
from .group import GroupApi
|
||||||
from .guild import GuildApi
|
from .guild import GuildApi
|
||||||
|
from .panel import PanelApi
|
||||||
from .v2 import V2Api, button, keyboard
|
from .v2 import V2Api, button, keyboard
|
||||||
|
|
||||||
|
|
||||||
class BotApi(V2Api, ChannelApi, GuildApi):
|
class BotApi(V2Api, ChannelApi, GuildApi, PanelApi, GroupApi, AudioApi, ForumApi):
|
||||||
"""通用 REST 封装;具体业务接口见各域方法集。
|
"""通用 REST 封装;具体业务接口见各域方法集。
|
||||||
|
|
||||||
BaseApi(请求内核)经域方法集继承自然落在 MRO 末尾,
|
BaseApi(请求内核)经域方法集继承自然落在 MRO 末尾,
|
||||||
@@ -21,9 +26,13 @@ class BotApi(V2Api, ChannelApi, GuildApi):
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ApiError",
|
"ApiError",
|
||||||
|
"AudioApi",
|
||||||
"BotApi",
|
"BotApi",
|
||||||
"ChannelApi",
|
"ChannelApi",
|
||||||
|
"ForumApi",
|
||||||
|
"GroupApi",
|
||||||
"GuildApi",
|
"GuildApi",
|
||||||
|
"PanelApi",
|
||||||
"V2Api",
|
"V2Api",
|
||||||
"button",
|
"button",
|
||||||
"keyboard",
|
"keyboard",
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""音频方法集(仅音频类机器人):子频道播放控制与上/下麦。
|
||||||
|
|
||||||
|
需联系平台开通权限后调用;控制与上/下麦均触发 AUDIO_ACTION 事件。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .core import BaseApi
|
||||||
|
|
||||||
|
# 播放状态(AudioControl.status)
|
||||||
|
AUDIO_STATUS_START = 0
|
||||||
|
AUDIO_STATUS_PAUSE = 1
|
||||||
|
AUDIO_STATUS_RESUME = 2
|
||||||
|
AUDIO_STATUS_STOP = 3
|
||||||
|
|
||||||
|
|
||||||
|
class AudioApi(BaseApi):
|
||||||
|
"""音频方法集;请求内核(鉴权/重试/错误)由 BaseApi 提供。"""
|
||||||
|
|
||||||
|
async def control_channel_audio(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
audio_url: str | None = None,
|
||||||
|
text: str | None = None,
|
||||||
|
status: int = AUDIO_STATUS_START,
|
||||||
|
) -> dict:
|
||||||
|
"""控制子频道音频播放(POST /channels/{channel_id}/audio)。
|
||||||
|
|
||||||
|
status 为 START/PAUSE/RESUME/STOP(AUDIO_STATUS_* 常量);
|
||||||
|
audio_url 与状态文本 text 仅 START 时传。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {"status": status}
|
||||||
|
if audio_url is not None:
|
||||||
|
body["audio_url"] = audio_url
|
||||||
|
if text is not None:
|
||||||
|
body["text"] = text
|
||||||
|
return await self.post(f"/channels/{channel_id}/audio", json=body)
|
||||||
|
|
||||||
|
async def put_channel_mic(self, channel_id: str) -> dict:
|
||||||
|
"""机器人上麦(PUT /channels/{channel_id}/mic)。"""
|
||||||
|
return await self.put(f"/channels/{channel_id}/mic")
|
||||||
|
|
||||||
|
async def delete_channel_mic(self, channel_id: str) -> dict:
|
||||||
|
"""机器人下麦(DELETE /channels/{channel_id}/mic)。"""
|
||||||
|
return await self.delete(f"/channels/{channel_id}/mic")
|
||||||
+120
-9
@@ -1,10 +1,30 @@
|
|||||||
"""频道消息方法集(channel_id 接入面):发送、撤回与表情表态。"""
|
"""频道消息方法集(channel_id 接入面):发送、撤回、表情表态、
|
||||||
|
子频道权限与频道私信。"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .core import BaseApi
|
from .core import BaseApi
|
||||||
|
|
||||||
|
|
||||||
|
def _message_body(
|
||||||
|
content: str,
|
||||||
|
msg_id: str | None,
|
||||||
|
msg_seq: int,
|
||||||
|
image: str | None,
|
||||||
|
extra: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""频道系消息(频道/私信)的请求体:msg_seq 必带,其余按需。"""
|
||||||
|
body: dict[str, Any] = {"msg_seq": msg_seq}
|
||||||
|
if content:
|
||||||
|
body["content"] = content
|
||||||
|
if msg_id:
|
||||||
|
body["msg_id"] = msg_id
|
||||||
|
if image:
|
||||||
|
body["image"] = image
|
||||||
|
body.update(extra)
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
class ChannelApi(BaseApi):
|
class ChannelApi(BaseApi):
|
||||||
"""频道消息方法集;请求内核(鉴权/重试/错误)由 BaseApi 提供。"""
|
"""频道消息方法集;请求内核(鉴权/重试/错误)由 BaseApi 提供。"""
|
||||||
|
|
||||||
@@ -21,14 +41,7 @@ class ChannelApi(BaseApi):
|
|||||||
|
|
||||||
image 为图片 URL;其余字段(embed/ark/markdown 等)经 extra 透传。
|
image 为图片 URL;其余字段(embed/ark/markdown 等)经 extra 透传。
|
||||||
"""
|
"""
|
||||||
body: dict[str, Any] = {"msg_seq": msg_seq}
|
body = _message_body(content, msg_id, msg_seq, image, extra)
|
||||||
if content:
|
|
||||||
body["content"] = content
|
|
||||||
if msg_id:
|
|
||||||
body["msg_id"] = msg_id
|
|
||||||
if image:
|
|
||||||
body["image"] = image
|
|
||||||
body.update(extra)
|
|
||||||
return await self.post(f"/channels/{channel_id}/messages", json=body)
|
return await self.post(f"/channels/{channel_id}/messages", json=body)
|
||||||
|
|
||||||
async def withdraw_channel_message(
|
async def withdraw_channel_message(
|
||||||
@@ -64,3 +77,101 @@ class ChannelApi(BaseApi):
|
|||||||
f"/channels/{channel_id}/messages/{message_id}"
|
f"/channels/{channel_id}/messages/{message_id}"
|
||||||
f"/reactions/{emoji_type}/{emoji_id}"
|
f"/reactions/{emoji_type}/{emoji_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---------- 子频道权限 ----------
|
||||||
|
|
||||||
|
async def get_channel_permissions(self, channel_id: str, user_id: str) -> dict:
|
||||||
|
"""获取子频道指定用户的权限(GET .../members/{user_id}/permissions)。
|
||||||
|
|
||||||
|
返回 permissions 字符串形式的权限位图(各权限位含义见官方文档)。
|
||||||
|
"""
|
||||||
|
return await self.get(f"/channels/{channel_id}/members/{user_id}/permissions")
|
||||||
|
|
||||||
|
async def get_channel_role_permissions(
|
||||||
|
self, channel_id: str, role_id: str
|
||||||
|
) -> dict:
|
||||||
|
"""获取子频道指定身份组的权限(GET .../roles/{role_id}/permissions)。"""
|
||||||
|
return await self.get(f"/channels/{channel_id}/roles/{role_id}/permissions")
|
||||||
|
|
||||||
|
async def update_channel_permissions(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
user_id: str,
|
||||||
|
*,
|
||||||
|
add: str | None = None,
|
||||||
|
remove: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""修改子频道指定用户的权限(PUT .../members/{user_id}/permissions)。
|
||||||
|
|
||||||
|
add/remove 为字符串形式的权限位图(要赋予/删除的权限位),
|
||||||
|
至少传一个;需要管理员权限。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {}
|
||||||
|
if add is not None:
|
||||||
|
body["add"] = add
|
||||||
|
if remove is not None:
|
||||||
|
body["remove"] = remove
|
||||||
|
return await self.put(
|
||||||
|
f"/channels/{channel_id}/members/{user_id}/permissions", json=body
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update_channel_role_permissions(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
role_id: str,
|
||||||
|
*,
|
||||||
|
add: str | None = None,
|
||||||
|
remove: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""修改子频道指定身份组的权限(PUT .../roles/{role_id}/permissions)。"""
|
||||||
|
body: dict[str, Any] = {}
|
||||||
|
if add is not None:
|
||||||
|
body["add"] = add
|
||||||
|
if remove is not None:
|
||||||
|
body["remove"] = remove
|
||||||
|
return await self.put(
|
||||||
|
f"/channels/{channel_id}/roles/{role_id}/permissions", json=body
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 频道私信 ----------
|
||||||
|
|
||||||
|
async def create_dms(self, recipient_id: str, source_guild_id: str) -> dict:
|
||||||
|
"""创建私信会话(POST /users/@me/dms)。
|
||||||
|
|
||||||
|
机器人和用户存在共同频道才能创建;返回私信的 guild_id 与
|
||||||
|
channel_id,后续发/撤私信用它作路径参数。
|
||||||
|
"""
|
||||||
|
return await self.post(
|
||||||
|
"/users/@me/dms",
|
||||||
|
json={"recipient_id": recipient_id, "source_guild_id": source_guild_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def post_dms_message(
|
||||||
|
self,
|
||||||
|
guild_id: str,
|
||||||
|
content: str = "",
|
||||||
|
msg_id: str | None = None,
|
||||||
|
msg_seq: int = 1,
|
||||||
|
image: str | None = None,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""发私信消息(POST /dms/{guild_id}/messages)。
|
||||||
|
|
||||||
|
guild_id 为 create_dms 返回(或 DIRECT_MESSAGE_CREATE 事件携带)
|
||||||
|
的私信频道 id;参数与 post_channel_message 一致。私信每天可对单
|
||||||
|
用户发 2 条主动消息、累计 200 条,被动消息不限。
|
||||||
|
"""
|
||||||
|
body = _message_body(content, msg_id, msg_seq, image, extra)
|
||||||
|
return await self.post(f"/dms/{guild_id}/messages", json=body)
|
||||||
|
|
||||||
|
async def withdraw_dms_message(
|
||||||
|
self, guild_id: str, message_id: str, hide_tip: bool = False
|
||||||
|
) -> dict:
|
||||||
|
"""撤回私信消息(DELETE /dms/{guild_id}/messages/{message_id})。私域接口。
|
||||||
|
|
||||||
|
只能撤回机器人自己发送的私信;hide_tip 隐藏撤回提示小灰条。
|
||||||
|
"""
|
||||||
|
return await self.delete(
|
||||||
|
f"/dms/{guild_id}/messages/{message_id}",
|
||||||
|
params={"hidetip": str(hide_tip).lower()},
|
||||||
|
)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""论坛方法集(私域,仅论坛子频道):帖子列表、详情、发帖与删帖。
|
||||||
|
|
||||||
|
发帖/删帖为异步任务(返回 task_id),结果经 FORUM_* 事件推送;
|
||||||
|
content 为 FORMAT_JSON 时参照官方 RichText 结构。仅私域机器人可用。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .core import BaseApi
|
||||||
|
|
||||||
|
# 帖子内容格式(format)
|
||||||
|
FORUM_FORMAT_TEXT = 1
|
||||||
|
FORUM_FORMAT_HTML = 2
|
||||||
|
FORUM_FORMAT_MARKDOWN = 3
|
||||||
|
FORUM_FORMAT_JSON = 4
|
||||||
|
|
||||||
|
|
||||||
|
class ForumApi(BaseApi):
|
||||||
|
"""论坛方法集;请求内核(鉴权/重试/错误)由 BaseApi 提供。"""
|
||||||
|
|
||||||
|
async def get_channel_threads(self, channel_id: str) -> dict:
|
||||||
|
"""获取帖子列表(GET /channels/{channel_id}/threads)。
|
||||||
|
|
||||||
|
返回 {"threads": [...], "is_finish": ...}(1 表示已拉取完毕);
|
||||||
|
threads 内 content 为 RichText 结构。
|
||||||
|
"""
|
||||||
|
return await self.get(f"/channels/{channel_id}/threads")
|
||||||
|
|
||||||
|
async def get_channel_thread(self, channel_id: str, thread_id: str) -> dict:
|
||||||
|
"""获取帖子详情(GET .../threads/{thread_id}),含首楼与回复。"""
|
||||||
|
return await self.get(f"/channels/{channel_id}/threads/{thread_id}")
|
||||||
|
|
||||||
|
async def put_channel_thread(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
title: str,
|
||||||
|
content: str,
|
||||||
|
format: int = FORUM_FORMAT_TEXT,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""发表帖子(PUT /channels/{channel_id}/threads)。
|
||||||
|
|
||||||
|
format 为 FORUM_FORMAT_* 常量;返回 {"task_id": ...},发表结果
|
||||||
|
经 FORUM_THREAD_CREATE 事件推送。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {
|
||||||
|
"title": title,
|
||||||
|
"content": content,
|
||||||
|
"format": format,
|
||||||
|
}
|
||||||
|
body.update(extra)
|
||||||
|
return await self.put(f"/channels/{channel_id}/threads", json=body)
|
||||||
|
|
||||||
|
async def delete_channel_thread(self, channel_id: str, thread_id: str) -> dict:
|
||||||
|
"""删除帖子(DELETE .../threads/{thread_id})。
|
||||||
|
|
||||||
|
返回 {"task_id": ...},删除结果经 FORUM_THREAD_DELETE 事件推送。
|
||||||
|
"""
|
||||||
|
return await self.delete(f"/channels/{channel_id}/threads/{thread_id}")
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
"""群管理方法集(群 openid 接入面):群信息、成员、机器人群内状态、
|
||||||
|
入群申请审批、群黑名单、禁言查询与入群自动审批策略。
|
||||||
|
|
||||||
|
多数接口要求机器人拥有群管理员身份;成员/黑名单/批量移除等能力
|
||||||
|
(官方标注"内邀接入中")与群信息等接口仅白名单机器人可用,未开通时
|
||||||
|
服务端返回 11253(ApiError.code)。入群自动审批策略是机器人维度的
|
||||||
|
全局配置(路径不含群 openid),关联群时 group_openids 与 group_ids
|
||||||
|
二选一(后者为 QQ 群号),最多 100 个,一个机器人最多 20 个策略。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .core import BaseApi
|
||||||
|
|
||||||
|
# 审批/黑名单/白名单的操作取值
|
||||||
|
OP_ADD = "add"
|
||||||
|
OP_DEL = "del"
|
||||||
|
APPROVE = "approve"
|
||||||
|
DECLINE = "decline"
|
||||||
|
|
||||||
|
|
||||||
|
class GroupApi(BaseApi):
|
||||||
|
"""群管理方法集;请求内核(鉴权/重试/错误)由 BaseApi 提供。"""
|
||||||
|
|
||||||
|
# ---------- 群信息与成员 ----------
|
||||||
|
|
||||||
|
async def get_group_info(self, group_openid: str) -> dict:
|
||||||
|
"""获取群基本信息(GET /v2/groups/{group_openid}/info)。
|
||||||
|
|
||||||
|
返回群名称/简介/分类/标签/成员数等;仅白名单机器人可用。
|
||||||
|
"""
|
||||||
|
return await self.get(f"/v2/groups/{group_openid}/info")
|
||||||
|
|
||||||
|
async def get_group_members(self, group_openid: str, cursor: str | None = None) -> dict:
|
||||||
|
"""获取群成员列表(GET /v2/groups/{group_openid}/members)。
|
||||||
|
|
||||||
|
每次最多返回 30 条,翻页透传上次响应的 next_cursor(空串=末页)。
|
||||||
|
"""
|
||||||
|
params = {"cursor": cursor} if cursor else None
|
||||||
|
return await self.get(f"/v2/groups/{group_openid}/members", params=params)
|
||||||
|
|
||||||
|
async def get_group_member(self, group_openid: str, member_openid: str) -> dict:
|
||||||
|
"""获取指定群成员信息(GET .../members/{member_openid})。
|
||||||
|
|
||||||
|
返回昵称、角色(member/owner/admin)、入群时间等。
|
||||||
|
"""
|
||||||
|
return await self.get(
|
||||||
|
f"/v2/groups/{group_openid}/members/{member_openid}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_group_bot_state(self, group_openid: str) -> dict:
|
||||||
|
"""获取机器人在指定群内的状态(GET .../bot_state)。
|
||||||
|
|
||||||
|
返回机器人入群时间、是否允许主动推送(allow_proactive_msg)、
|
||||||
|
群内消息接收设置(recv_msg_setting)与自身角色。
|
||||||
|
"""
|
||||||
|
return await self.get(f"/v2/groups/{group_openid}/bot_state")
|
||||||
|
|
||||||
|
async def remove_group_members(
|
||||||
|
self,
|
||||||
|
group_openid: str,
|
||||||
|
member_openids: list[str],
|
||||||
|
add_to_member_blacklist: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""批量移除群成员(POST .../batch_remove_members),单次最多 20 个。
|
||||||
|
|
||||||
|
add_to_member_blacklist=True 同时加入群黑名单;返回含拉黑失败
|
||||||
|
的 openid 列表。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {"member_openids": member_openids}
|
||||||
|
if add_to_member_blacklist:
|
||||||
|
body["add_to_member_blacklist"] = True
|
||||||
|
return await self.post(
|
||||||
|
f"/v2/groups/{group_openid}/batch_remove_members", json=body
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 入群申请 ----------
|
||||||
|
|
||||||
|
async def get_group_join_requests(
|
||||||
|
self, group_openid: str, cursor: str | None = None, limit: int | None = None
|
||||||
|
) -> dict:
|
||||||
|
"""拉取入群申请列表(GET .../join_request_list),需群管理员身份。
|
||||||
|
|
||||||
|
返回 {"list": [...], "next_cursor": ...};申请含 join_request_id
|
||||||
|
(审批时回传)、验证方式与问答内容。limit 默认 20、最大 50。
|
||||||
|
"""
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
if cursor:
|
||||||
|
params["cursor"] = cursor
|
||||||
|
if limit is not None:
|
||||||
|
params["limit"] = limit
|
||||||
|
return await self.get(
|
||||||
|
f"/v2/groups/{group_openid}/join_request_list",
|
||||||
|
params=params or None,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def review_group_join_request(
|
||||||
|
self,
|
||||||
|
group_openid: str,
|
||||||
|
member_openid: str,
|
||||||
|
op: str,
|
||||||
|
join_request_id: str | None = None,
|
||||||
|
reject_reason: str | None = None,
|
||||||
|
add_to_member_blacklist: bool | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""审批入群申请(POST .../approval_join_request/{member_openid})。
|
||||||
|
|
||||||
|
op 为 APPROVE(通过)或 DECLINE(拒绝),需群管理员身份;
|
||||||
|
拒绝时可附 reject_reason 并以 add_to_member_blacklist=True
|
||||||
|
同时加入群黑名单。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {"op": op}
|
||||||
|
if join_request_id:
|
||||||
|
body["join_request_id"] = join_request_id
|
||||||
|
if reject_reason:
|
||||||
|
body["reject_reason"] = reject_reason
|
||||||
|
if add_to_member_blacklist is not None:
|
||||||
|
body["add_to_member_blacklist"] = add_to_member_blacklist
|
||||||
|
return await self.post(
|
||||||
|
f"/v2/groups/{group_openid}/approval_join_request/{member_openid}",
|
||||||
|
json=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 群黑名单与禁言查询 ----------
|
||||||
|
|
||||||
|
async def get_group_blacklist(
|
||||||
|
self, group_openid: str, cursor: str | None = None, limit: int | None = None
|
||||||
|
) -> dict:
|
||||||
|
"""查询群黑名单列表(GET .../member_blacklist),支持分页。
|
||||||
|
|
||||||
|
limit 默认 20、最大 100;返回 {"users": [...], "next_cursor": ...}。
|
||||||
|
"""
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
if cursor:
|
||||||
|
params["cursor"] = cursor
|
||||||
|
if limit is not None:
|
||||||
|
params["limit"] = limit
|
||||||
|
return await self.get(
|
||||||
|
f"/v2/groups/{group_openid}/member_blacklist", params=params or None
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update_group_blacklist(
|
||||||
|
self, group_openid: str, op: str, member_openids: list[str]
|
||||||
|
) -> dict:
|
||||||
|
"""群黑名单增删(POST .../member_blacklist),单次最多 20 个。
|
||||||
|
|
||||||
|
op 为 OP_ADD(加入)或 OP_DEL(移出);目标成员在群中时无法
|
||||||
|
加入黑名单(先移除再加)。返回失败的 openid 列表。
|
||||||
|
"""
|
||||||
|
return await self.post(
|
||||||
|
f"/v2/groups/{group_openid}/member_blacklist",
|
||||||
|
json={"op": op, "member_openids": member_openids},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_group_mute_state(self, group_openid: str) -> dict:
|
||||||
|
"""查询群禁言状态(GET .../restrict_chat_setting),需群管理员身份。
|
||||||
|
|
||||||
|
返回全员禁言规则(global_rule:定时/周期禁言)与当前被禁言的
|
||||||
|
成员列表(members,不含已过期)。
|
||||||
|
"""
|
||||||
|
return await self.get(f"/v2/groups/{group_openid}/restrict_chat_setting")
|
||||||
|
|
||||||
|
# ---------- 入群自动审批策略(机器人维度全局配置) ----------
|
||||||
|
|
||||||
|
async def get_join_approval_strategies(
|
||||||
|
self, cursor: str | None = None, limit: int | None = None
|
||||||
|
) -> dict:
|
||||||
|
"""查询入群自动审批策略列表(GET /v2/groups/join_approval_strategy)。
|
||||||
|
|
||||||
|
按创建时间倒序,返回 {"strategies": [...], "next_cursor": ...};
|
||||||
|
limit 默认 20、最大 50。
|
||||||
|
"""
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
if cursor:
|
||||||
|
params["cursor"] = cursor
|
||||||
|
if limit is not None:
|
||||||
|
params["limit"] = limit
|
||||||
|
return await self.get(
|
||||||
|
"/v2/groups/join_approval_strategy", params=params or None
|
||||||
|
)
|
||||||
|
|
||||||
|
async def create_join_approval_strategy(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
group_openids: list[str] | None = None,
|
||||||
|
group_ids: list[str] | None = None,
|
||||||
|
is_enable: str | None = None,
|
||||||
|
expire_at: str | None = None,
|
||||||
|
remark: str | None = None,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""创建入群自动审批策略(POST /v2/groups/join_approval_strategy)。
|
||||||
|
|
||||||
|
group_openids 与 group_ids(QQ 群号,字符串避免 JS 精度问题)
|
||||||
|
二选一必填,互斥,最多 100 个;is_enable "on"/"off"(默认 on);
|
||||||
|
expire_at 为 RFC3339 过期时间(默认一年);策略仅当机器人拥有
|
||||||
|
群管理员身份时生效。返回服务端生成的 strategy_id。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {}
|
||||||
|
if group_openids is not None:
|
||||||
|
body["group_openids"] = group_openids
|
||||||
|
if group_ids is not None:
|
||||||
|
body["group_ids"] = group_ids
|
||||||
|
if is_enable is not None:
|
||||||
|
body["is_enable"] = is_enable
|
||||||
|
if expire_at is not None:
|
||||||
|
body["expire_at"] = expire_at
|
||||||
|
if remark is not None:
|
||||||
|
body["remark"] = remark
|
||||||
|
body.update(extra)
|
||||||
|
return await self.post("/v2/groups/join_approval_strategy", json=body)
|
||||||
|
|
||||||
|
async def update_join_approval_strategy(
|
||||||
|
self,
|
||||||
|
strategy_id: str,
|
||||||
|
*,
|
||||||
|
is_enable: str | None = None,
|
||||||
|
expire_at: str | None = None,
|
||||||
|
group_action: dict | None = None,
|
||||||
|
remark: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""修改入群自动审批策略(PATCH /v2/groups/join_approval_strategy/{id})。
|
||||||
|
|
||||||
|
只传变更项;group_action 为 {"op": "add"/"del", "group_openids":
|
||||||
|
[...]} 或带 "group_ids",群标识形式须与创建时一致。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {}
|
||||||
|
if is_enable is not None:
|
||||||
|
body["is_enable"] = is_enable
|
||||||
|
if expire_at is not None:
|
||||||
|
body["expire_at"] = expire_at
|
||||||
|
if group_action is not None:
|
||||||
|
body["group_action"] = group_action
|
||||||
|
if remark is not None:
|
||||||
|
body["remark"] = remark
|
||||||
|
return await self.patch(
|
||||||
|
f"/v2/groups/join_approval_strategy/{strategy_id}", json=body
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete_join_approval_strategy(self, strategy_id: str) -> dict:
|
||||||
|
"""删除入群自动审批策略(DELETE /v2/groups/join_approval_strategy/{id})。"""
|
||||||
|
return await self.delete(f"/v2/groups/join_approval_strategy/{strategy_id}")
|
||||||
|
|
||||||
|
async def execute_join_approval_strategy(self, strategy_id: str) -> dict:
|
||||||
|
"""执行入群自动审批策略(POST .../{strategy_id}/execute)。
|
||||||
|
|
||||||
|
对策略关联的全部群发起全量扫描,命中白名单号码的入群申请自动
|
||||||
|
审批通过;异步执行,约 10 分钟完成。
|
||||||
|
"""
|
||||||
|
return await self.post(f"/v2/groups/join_approval_strategy/{strategy_id}/execute")
|
||||||
|
|
||||||
|
async def update_join_approval_strategy_whitelist(
|
||||||
|
self, strategy_id: str, op: str, whitelist_users: list[str]
|
||||||
|
) -> dict:
|
||||||
|
"""增删策略白名单 QQ 号码(POST .../{strategy_id}/whitelist_users)。
|
||||||
|
|
||||||
|
op 为 OP_ADD/OP_DEL;whitelist_users 为 QQ 号码字符串列表
|
||||||
|
(避免 JS 精度问题),单次最多 10000 个,号码上限 10 万。
|
||||||
|
"""
|
||||||
|
return await self.post(
|
||||||
|
f"/v2/groups/join_approval_strategy/{strategy_id}/whitelist_users",
|
||||||
|
json={"op": op, "whitelist_users": whitelist_users},
|
||||||
|
)
|
||||||
@@ -130,18 +130,242 @@ class GuildApi(BaseApi):
|
|||||||
"""获取频道信息(GET /guilds/{guild_id})。"""
|
"""获取频道信息(GET /guilds/{guild_id})。"""
|
||||||
return await self.get(f"/guilds/{guild_id}")
|
return await self.get(f"/guilds/{guild_id}")
|
||||||
|
|
||||||
|
async def get_me_guilds(
|
||||||
|
self,
|
||||||
|
before: str | None = None,
|
||||||
|
after: str | None = None,
|
||||||
|
limit: int | None = None,
|
||||||
|
) -> list | dict:
|
||||||
|
"""获取机器人加入的频道列表(GET /users/@me/guilds)。
|
||||||
|
|
||||||
|
before/after 为分页锚点 guild_id(同时传时 after 无效);limit
|
||||||
|
默认 100、最大 100。
|
||||||
|
"""
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
if before:
|
||||||
|
params["before"] = before
|
||||||
|
if after:
|
||||||
|
params["after"] = after
|
||||||
|
if limit is not None:
|
||||||
|
params["limit"] = limit
|
||||||
|
return await self.get("/users/@me/guilds", params=params or None)
|
||||||
|
|
||||||
async def get_guild_channels(self, guild_id: str) -> list | dict:
|
async def get_guild_channels(self, guild_id: str) -> list | dict:
|
||||||
"""获取频道下的子频道列表(GET /channels/{guild_id}/channels)。"""
|
"""获取频道下的子频道列表(GET /channels/{guild_id}/channels)。"""
|
||||||
return await self.get(f"/channels/{guild_id}/channels")
|
return await self.get(f"/channels/{guild_id}/channels")
|
||||||
|
|
||||||
|
async def create_channel(self, guild_id: str, name: str, **fields: Any) -> dict:
|
||||||
|
"""创建子频道(POST /guilds/{guild_id}/channels)。私域接口,需管理员权限。
|
||||||
|
|
||||||
|
fields 透传 type/sub_type/position/parent_id/private_type/
|
||||||
|
private_user_ids/speak_permission/application_id;成功后触发
|
||||||
|
CHANNEL_CREATE 事件。
|
||||||
|
"""
|
||||||
|
return await self.post(
|
||||||
|
f"/guilds/{guild_id}/channels", json={"name": name, **fields}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update_channel(self, channel_id: str, **fields: Any) -> dict:
|
||||||
|
"""修改子频道(PATCH /channels/{channel_id})。私域接口,需管理员权限。
|
||||||
|
|
||||||
|
fields 为要修改的字段(name/position/parent_id/private_type/
|
||||||
|
speak_permission),只需传变更项;成功后触发 CHANNEL_UPDATE 事件。
|
||||||
|
"""
|
||||||
|
return await self.patch(f"/channels/{channel_id}", json=fields)
|
||||||
|
|
||||||
|
async def delete_channel(self, channel_id: str) -> dict:
|
||||||
|
"""删除子频道(DELETE /channels/{channel_id})。私域接口,需管理员权限;
|
||||||
|
删除后不可恢复,成功后触发 CHANNEL_DELETE 事件。"""
|
||||||
|
return await self.delete(f"/channels/{channel_id}")
|
||||||
|
|
||||||
async def get_channel(self, channel_id: str) -> dict:
|
async def get_channel(self, channel_id: str) -> dict:
|
||||||
"""获取子频道信息(GET /channels/{channel_id})。"""
|
"""获取子频道信息(GET /channels/{channel_id})。"""
|
||||||
return await self.get(f"/channels/{channel_id}")
|
return await self.get(f"/channels/{channel_id}")
|
||||||
|
|
||||||
|
async def get_channel_online_nums(self, channel_id: str) -> dict:
|
||||||
|
"""获取子频道在线成员数(GET /channels/{channel_id}/online_nums)。"""
|
||||||
|
return await self.get(f"/channels/{channel_id}/online_nums")
|
||||||
|
|
||||||
async def get_guild_roles(self, guild_id: str) -> dict:
|
async def get_guild_roles(self, guild_id: str) -> dict:
|
||||||
"""获取频道身份组列表(GET /guilds/{guild_id}/roles)。"""
|
"""获取频道身份组列表(GET /guilds/{guild_id}/roles)。"""
|
||||||
return await self.get(f"/guilds/{guild_id}/roles")
|
return await self.get(f"/guilds/{guild_id}/roles")
|
||||||
|
|
||||||
|
async def create_guild_role(
|
||||||
|
self,
|
||||||
|
guild_id: str,
|
||||||
|
name: str = "",
|
||||||
|
color: int | None = None,
|
||||||
|
hoist: int | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""创建频道身份组(POST /guilds/{guild_id}/roles)。
|
||||||
|
|
||||||
|
color 为 ARGB HEX 十六进制颜色值转十进制;hoist=1 在成员列表
|
||||||
|
中单独展示。返回 {"role_id": ..., "role": {...}}。需要管理员权限。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {}
|
||||||
|
if name:
|
||||||
|
body["name"] = name
|
||||||
|
if color is not None:
|
||||||
|
body["color"] = color
|
||||||
|
if hoist is not None:
|
||||||
|
body["hoist"] = hoist
|
||||||
|
return await self.post(f"/guilds/{guild_id}/roles", json=body)
|
||||||
|
|
||||||
|
async def update_guild_role(
|
||||||
|
self,
|
||||||
|
guild_id: str,
|
||||||
|
role_id: str,
|
||||||
|
name: str | None = None,
|
||||||
|
color: int | None = None,
|
||||||
|
hoist: int | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""修改频道身份组(PATCH /guilds/{guild_id}/roles/{role_id})。
|
||||||
|
|
||||||
|
只传变更项;默认身份组(role_id=1~5,见官方文档)不可修改。
|
||||||
|
需要管理员权限。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {}
|
||||||
|
if name is not None:
|
||||||
|
body["name"] = name
|
||||||
|
if color is not None:
|
||||||
|
body["color"] = color
|
||||||
|
if hoist is not None:
|
||||||
|
body["hoist"] = hoist
|
||||||
|
return await self.patch(f"/guilds/{guild_id}/roles/{role_id}", json=body)
|
||||||
|
|
||||||
|
async def delete_guild_role(self, guild_id: str, role_id: str) -> dict:
|
||||||
|
"""删除频道身份组(DELETE /guilds/{guild_id}/roles/{role_id})。
|
||||||
|
|
||||||
|
只能删除自己创建的身份组;需要管理员权限。
|
||||||
|
"""
|
||||||
|
return await self.delete(f"/guilds/{guild_id}/roles/{role_id}")
|
||||||
|
|
||||||
|
async def add_guild_member_role(
|
||||||
|
self, guild_id: str, user_id: str, role_id: str
|
||||||
|
) -> dict:
|
||||||
|
"""给成员授予身份组(PUT /guilds/{guild_id}/members/{user_id}/roles/{role_id})。
|
||||||
|
|
||||||
|
需要管理员权限(同身份组的成员也可操作)。
|
||||||
|
"""
|
||||||
|
return await self.put(
|
||||||
|
f"/guilds/{guild_id}/members/{user_id}/roles/{role_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def remove_guild_member_role(
|
||||||
|
self, guild_id: str, user_id: str, role_id: str
|
||||||
|
) -> dict:
|
||||||
|
"""删除成员的身份组(DELETE .../roles/{role_id})。需要管理员权限。"""
|
||||||
|
return await self.delete(
|
||||||
|
f"/guilds/{guild_id}/members/{user_id}/roles/{role_id}"
|
||||||
|
)
|
||||||
|
|
||||||
async def get_guild_member(self, guild_id: str, user_id: str) -> dict:
|
async def get_guild_member(self, guild_id: str, user_id: str) -> dict:
|
||||||
"""获取成员详情(GET /guilds/{guild_id}/members/{user_id})。"""
|
"""获取成员详情(GET /guilds/{guild_id}/members/{user_id})。"""
|
||||||
return await self.get(f"/guilds/{guild_id}/members/{user_id}")
|
return await self.get(f"/guilds/{guild_id}/members/{user_id}")
|
||||||
|
|
||||||
|
async def get_guild_members(self, guild_id: str) -> list | dict:
|
||||||
|
"""获取频道成员列表(GET /guilds/{guild_id}/members)。
|
||||||
|
|
||||||
|
1 分钟内最多获取 20 次,每次最多 100 人;仅私域机器人可用。
|
||||||
|
"""
|
||||||
|
return await self.get(f"/guilds/{guild_id}/members")
|
||||||
|
|
||||||
|
async def remove_guild_member(
|
||||||
|
self,
|
||||||
|
guild_id: str,
|
||||||
|
user_id: str,
|
||||||
|
add_blacklist: bool = False,
|
||||||
|
delete_history_msg_days: int = 0,
|
||||||
|
) -> dict:
|
||||||
|
"""踢出频道成员(DELETE /guilds/{guild_id}/members/{user_id})。私域接口。
|
||||||
|
|
||||||
|
需要机器人具备踢人权限(管理员);无法移除管理员。add_blacklist
|
||||||
|
同时拉黑;delete_history_msg_days 撤回其消息,仅支持 3/7/15/30
|
||||||
|
或 -1(全部),默认 0 不撤回。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {}
|
||||||
|
if add_blacklist:
|
||||||
|
body["add_blacklist"] = True
|
||||||
|
if delete_history_msg_days:
|
||||||
|
body["delete_history_msg_days"] = delete_history_msg_days
|
||||||
|
return await self.delete(
|
||||||
|
f"/guilds/{guild_id}/members/{user_id}", json=body or None
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_role_members(self, guild_id: str, role_id: str) -> list | dict:
|
||||||
|
"""获取身份组成员列表(GET /guilds/{guild_id}/roles/{role_id}/members)。
|
||||||
|
|
||||||
|
1 分钟内最多获取 20 次,每次最多 100 人;仅私域机器人可用。
|
||||||
|
"""
|
||||||
|
return await self.get(f"/guilds/{guild_id}/roles/{role_id}/members")
|
||||||
|
|
||||||
|
# ---------- 禁言与消息设置 ----------
|
||||||
|
|
||||||
|
async def mute_guild(
|
||||||
|
self,
|
||||||
|
guild_id: str,
|
||||||
|
mute_end_timestamp: str | None = None,
|
||||||
|
mute_seconds: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""频道全员禁言(PATCH /guilds/{guild_id}/mute)。
|
||||||
|
|
||||||
|
mute_end_timestamp(秒级绝对时间戳)与 mute_seconds(字符串秒数)
|
||||||
|
二选一,同时传以前者为准;解除全员禁言两个传 "0"。需要管理员权限。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {}
|
||||||
|
if mute_end_timestamp is not None:
|
||||||
|
body["mute_end_timestamp"] = mute_end_timestamp
|
||||||
|
if mute_seconds is not None:
|
||||||
|
body["mute_seconds"] = mute_seconds
|
||||||
|
return await self.patch(f"/guilds/{guild_id}/mute", json=body)
|
||||||
|
|
||||||
|
async def mute_guild_members(
|
||||||
|
self,
|
||||||
|
guild_id: str,
|
||||||
|
user_ids: list[str],
|
||||||
|
mute_end_timestamp: str | None = None,
|
||||||
|
mute_seconds: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""频道批量成员禁言(PATCH /guilds/{guild_id}/mute,带 user_ids)。
|
||||||
|
|
||||||
|
一次最多禁言 20 人;时间字段规则同 mute_guild,解除时传 "0"。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {"user_ids": user_ids}
|
||||||
|
if mute_end_timestamp is not None:
|
||||||
|
body["mute_end_timestamp"] = mute_end_timestamp
|
||||||
|
if mute_seconds is not None:
|
||||||
|
body["mute_seconds"] = mute_seconds
|
||||||
|
return await self.patch(f"/guilds/{guild_id}/mute", json=body)
|
||||||
|
|
||||||
|
async def get_guild_message_setting(self, guild_id: str) -> dict:
|
||||||
|
"""获取频道消息频率设置详情(GET /guilds/{guild_id}/message/setting)。"""
|
||||||
|
return await self.get(f"/guilds/{guild_id}/message/setting")
|
||||||
|
|
||||||
|
# ---------- 接口权限 ----------
|
||||||
|
|
||||||
|
async def get_guild_api_permissions(self, guild_id: str) -> dict:
|
||||||
|
"""获取机器人在频道内的可用接口权限列表(GET .../api_permission)。"""
|
||||||
|
return await self.get(f"/guilds/{guild_id}/api_permission")
|
||||||
|
|
||||||
|
async def create_api_permission_demand(
|
||||||
|
self,
|
||||||
|
guild_id: str,
|
||||||
|
channel_id: str,
|
||||||
|
path: str,
|
||||||
|
method: str,
|
||||||
|
desc: str,
|
||||||
|
) -> dict:
|
||||||
|
"""申请频道接口权限(POST /guilds/{guild_id}/api_permission/demand)。
|
||||||
|
|
||||||
|
生成权限授权链接发到子频道 channel_id,由频道管理员点击授权;
|
||||||
|
path/method 为要申请的接口(如 "/channels/{channel_id}/messages",
|
||||||
|
method 为 GET/POST…),desc 说明申请后机器人能实现的功能。
|
||||||
|
"""
|
||||||
|
return await self.post(
|
||||||
|
f"/guilds/{guild_id}/api_permission/demand",
|
||||||
|
json={
|
||||||
|
"channel_id": channel_id,
|
||||||
|
"api_identify": {"path": path, "method": method},
|
||||||
|
"desc": desc,
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
"""指令面板(斜线指令)与全局自定义菜单方法集。
|
||||||
|
|
||||||
|
指令面板即用户在聊天输入框输入 "/" 唤起的指令列表,支持 c2c(单聊)、
|
||||||
|
group(群聊)、channel(文字子频道)、dm(频道私信)四种场景;c2c/group
|
||||||
|
支持 target_type=specific 按指定对象生效(关联对象每次最多 20 个,面板
|
||||||
|
详情最多查 1000 个),channel/dm 仅全局生效。一个机器人最多 20 个面板,
|
||||||
|
每个面板最多 20 个元素(name ≤14 字符,desc ≤30 字符)。
|
||||||
|
|
||||||
|
全局自定义菜单(/v2/menu)仅 C2C 场景、对所有用户生效;send_message
|
||||||
|
类型点击后把文本(如 "/help")填入输入框,常与斜线指令配合引导用户。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .core import BaseApi
|
||||||
|
|
||||||
|
# scope 取值(生效场景)
|
||||||
|
SCOPE_C2C = "c2c"
|
||||||
|
SCOPE_GROUP = "group"
|
||||||
|
SCOPE_CHANNEL = "channel"
|
||||||
|
SCOPE_DM = "dm"
|
||||||
|
# target_type 取值(作用范围)
|
||||||
|
TARGET_ALL = "all"
|
||||||
|
TARGET_SPECIFIC = "specific"
|
||||||
|
# 面板元素 type 取值
|
||||||
|
ITEM_COMMAND = "command"
|
||||||
|
ITEM_LINK = "link"
|
||||||
|
# 关联对象操作 op 取值
|
||||||
|
TARGET_OP_ADD = "add"
|
||||||
|
TARGET_OP_DEL = "del"
|
||||||
|
|
||||||
|
|
||||||
|
class PanelApi(BaseApi):
|
||||||
|
"""指令面板与全局菜单方法集;请求内核(鉴权/重试/错误)由 BaseApi 提供。"""
|
||||||
|
|
||||||
|
async def create_panel(
|
||||||
|
self,
|
||||||
|
scope: str,
|
||||||
|
panel: dict,
|
||||||
|
*,
|
||||||
|
target_type: str | None = None,
|
||||||
|
user_openids: list[str] | None = None,
|
||||||
|
group_openids: list[str] | None = None,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""创建指令面板(POST /v2/panels),返回 {"panel_id": ...}。
|
||||||
|
|
||||||
|
panel 为 {"items": [...], "remark": ...} 结构,元素为
|
||||||
|
{"type": "command"/"link", "name", "desc", "only_admin", "link"}:
|
||||||
|
command 类型点击后 name 填入聊天输入框,link 类型点击跳转 link。
|
||||||
|
target_type=specific 时经 user_openids(仅 c2c)或 group_openids
|
||||||
|
(仅 group)指定生效对象;channel/dm 仅全局(target_type 只能 all)。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {"scope": scope, "panel": panel}
|
||||||
|
if target_type is not None:
|
||||||
|
body["target_type"] = target_type
|
||||||
|
if user_openids is not None:
|
||||||
|
body["user_openids"] = user_openids
|
||||||
|
if group_openids is not None:
|
||||||
|
body["group_openids"] = group_openids
|
||||||
|
body.update(extra)
|
||||||
|
return await self.post("/v2/panels", json=body)
|
||||||
|
|
||||||
|
async def get_panels(
|
||||||
|
self, scope: str, cursor: str | None = None, limit: int | None = None
|
||||||
|
) -> dict:
|
||||||
|
"""分页查询指令面板列表(GET /v2/panels),按设置时间倒序。
|
||||||
|
|
||||||
|
返回 {"records": [...], "next_cursor": ..., "is_end": ...};
|
||||||
|
next_cursor 为空串表示已到末页,翻页透传上次响应的 next_cursor。
|
||||||
|
limit 为每页条数,默认 20、最大 50。
|
||||||
|
"""
|
||||||
|
params: dict[str, Any] = {"scope": scope}
|
||||||
|
if cursor:
|
||||||
|
params["cursor"] = cursor
|
||||||
|
if limit is not None:
|
||||||
|
params["limit"] = limit
|
||||||
|
return await self.get("/v2/panels", params=params)
|
||||||
|
|
||||||
|
async def get_panel(self, panel_id: str) -> dict:
|
||||||
|
"""查询指令面板详情(GET /v2/panels/{panel_id})。
|
||||||
|
|
||||||
|
返回面板配置与生效场景/范围;specific 面板另带关联的
|
||||||
|
user_openids / group_openids 列表(最多 1000 条)。
|
||||||
|
"""
|
||||||
|
return await self.get(f"/v2/panels/{panel_id}")
|
||||||
|
|
||||||
|
async def update_panel(self, panel_id: str, panel: dict) -> dict:
|
||||||
|
"""修改指令面板(PUT /v2/panels/{panel_id}),返回 {"version": ...}。
|
||||||
|
|
||||||
|
panel 整体覆盖面板元素列表与备注,不影响已关联的用户/群列表。
|
||||||
|
"""
|
||||||
|
return await self.put(f"/v2/panels/{panel_id}", json={"panel": panel})
|
||||||
|
|
||||||
|
async def delete_panel(self, panel_id: str) -> dict:
|
||||||
|
"""删除指令面板(DELETE /v2/panels/{panel_id}),删除后对所有对象失效。"""
|
||||||
|
return await self.delete(f"/v2/panels/{panel_id}")
|
||||||
|
|
||||||
|
async def update_panel_targets(
|
||||||
|
self,
|
||||||
|
panel_id: str,
|
||||||
|
op: str,
|
||||||
|
user_openids: list[str] | None = None,
|
||||||
|
group_openids: list[str] | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""增删面板关联对象(PUT /v2/panels/{panel_id}/target)。
|
||||||
|
|
||||||
|
op:"add" 添加 / "del" 移除;user_openids 仅 c2c、group_openids
|
||||||
|
仅 group,一次最多 20 个。channel/dm 场景与 target_type=all 的
|
||||||
|
全局面板不支持此操作。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {"op": op}
|
||||||
|
if user_openids is not None:
|
||||||
|
body["user_openids"] = user_openids
|
||||||
|
if group_openids is not None:
|
||||||
|
body["group_openids"] = group_openids
|
||||||
|
return await self.put(f"/v2/panels/{panel_id}/target", json=body)
|
||||||
|
|
||||||
|
# ---------- 全局自定义菜单(仅 C2C) ----------
|
||||||
|
|
||||||
|
async def get_menu(self) -> dict:
|
||||||
|
"""查询全局自定义菜单(GET /v2/menu),返回 {"version": ..., "menu": ...}。
|
||||||
|
|
||||||
|
menu 为 {"items": [...]},元素为 {"type": "switch"/"send_message"/
|
||||||
|
"link"/"menu", "name", "sub_menu_items", "send_message", "link",
|
||||||
|
"switch"};未设置过菜单时该字段为空。
|
||||||
|
"""
|
||||||
|
return await self.get("/v2/menu")
|
||||||
|
|
||||||
|
async def update_menu(self, menu: dict | None = None) -> dict:
|
||||||
|
"""修改全局自定义菜单(PUT /v2/menu),返回 {"version": ...}。
|
||||||
|
|
||||||
|
仅 C2C 场景、对所有用户生效;menu 为完整配置,传入后整体覆盖
|
||||||
|
(菜单项最多 10 个,子菜单最多 5 个且不支持再嵌套)。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {"menu": menu} if menu is not None else {}
|
||||||
|
return await self.put("/v2/menu", json=body)
|
||||||
+191
-2
@@ -1,5 +1,5 @@
|
|||||||
"""v2 群聊/单聊方法集(openid 接入面):消息、富媒体上传、撤回、禁言,
|
"""v2 群聊/单聊方法集(openid 接入面):消息、流式消息、富媒体上传与
|
||||||
以及内嵌按钮/键盘的快捷构造。
|
大文件分片上传、撤回、禁言、分享链接,以及内嵌按钮/键盘的快捷构造。
|
||||||
|
|
||||||
被动回复规则(官方文档 2026-09 核对):携带 msg_id(或 event_id,
|
被动回复规则(官方文档 2026-09 核对):携带 msg_id(或 event_id,
|
||||||
二选一)为被动回复——单聊 60 分钟内最多 4 次,群聊 5 分钟内最多 5 次;
|
二选一)为被动回复——单聊 60 分钟内最多 4 次,群聊 5 分钟内最多 5 次;
|
||||||
@@ -21,6 +21,13 @@ FILE_TYPE_IMAGE = 1
|
|||||||
FILE_TYPE_VIDEO = 2
|
FILE_TYPE_VIDEO = 2
|
||||||
FILE_TYPE_VOICE = 3
|
FILE_TYPE_VOICE = 3
|
||||||
FILE_TYPE_FILE = 4
|
FILE_TYPE_FILE = 4
|
||||||
|
# 流式消息取值
|
||||||
|
STREAM_INPUT_APPEND = "append" # 分片内容追加到待下发正文
|
||||||
|
STREAM_INPUT_REPLACE = "replace" # 分片内容为全量正文(须以上游已下发前缀开头)
|
||||||
|
STREAM_GENERATING = 1 # input_state:生成中
|
||||||
|
STREAM_FINISHED = 10 # input_state:生成结束
|
||||||
|
STREAM_CONTENT_TEXT = "text"
|
||||||
|
STREAM_CONTENT_MARKDOWN = "markdown"
|
||||||
|
|
||||||
|
|
||||||
def button(
|
def button(
|
||||||
@@ -105,6 +112,93 @@ class V2Api(BaseApi):
|
|||||||
f"/v2/users/{openid}", file_type, url, srv_send_msg
|
f"/v2/users/{openid}", file_type, url, srv_send_msg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def prepare_c2c_upload(
|
||||||
|
self,
|
||||||
|
openid: str,
|
||||||
|
file_type: int,
|
||||||
|
file_size: int | str,
|
||||||
|
file_name: str,
|
||||||
|
md5: str,
|
||||||
|
sha1: str,
|
||||||
|
md5_10m: str,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""单聊大文件分片预上传(POST /v2/users/{openid}/upload_prepare)。
|
||||||
|
|
||||||
|
用法同 prepare_group_upload;全部分片完成后携带 upload_id 调
|
||||||
|
upload_c2c_file 完成合并。
|
||||||
|
"""
|
||||||
|
return await self._prepare_upload(
|
||||||
|
f"/v2/users/{openid}",
|
||||||
|
file_type,
|
||||||
|
file_size,
|
||||||
|
file_name,
|
||||||
|
md5,
|
||||||
|
sha1,
|
||||||
|
md5_10m,
|
||||||
|
**extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def finish_c2c_upload_part(
|
||||||
|
self,
|
||||||
|
openid: str,
|
||||||
|
upload_id: str,
|
||||||
|
part_index: int,
|
||||||
|
block_size: int | str | None = None,
|
||||||
|
md5: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""单聊分片上传完成确认(POST .../upload_part_finish)。"""
|
||||||
|
return await self._finish_upload_part(
|
||||||
|
f"/v2/users/{openid}", upload_id, part_index, block_size, md5
|
||||||
|
)
|
||||||
|
|
||||||
|
async def post_c2c_stream_message(
|
||||||
|
self,
|
||||||
|
openid: str,
|
||||||
|
content_raw: str = "",
|
||||||
|
*,
|
||||||
|
stream_msg_id: str | None = None,
|
||||||
|
index: int = 0,
|
||||||
|
input_state: int = STREAM_GENERATING,
|
||||||
|
input_mode: str | None = None,
|
||||||
|
content_type: str = STREAM_CONTENT_MARKDOWN,
|
||||||
|
msg_id: str | None = None,
|
||||||
|
event_id: str | None = None,
|
||||||
|
msg_seq: int | None = None,
|
||||||
|
is_wakeup: bool | None = None,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""流式分批发送单聊消息(POST /v2/users/{openid}/stream_messages)。
|
||||||
|
|
||||||
|
每个分片使用相同 stream_msg_id、index 从 0 递增:首片不传
|
||||||
|
stream_msg_id,响应的 id 即后续分片要携带的 stream_msg_id;
|
||||||
|
以 input_state=STREAM_FINISHED 收尾。input_mode 默认 append
|
||||||
|
(分片拼接),replace 时 content_raw 须以上游已下发前缀开头。
|
||||||
|
content_type 默认 markdown;msg_id/event_id 二选一为被动回复;
|
||||||
|
is_wakeup=True 为召回消息,不校验 msg_id/event_id 有效期。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {
|
||||||
|
"index": index,
|
||||||
|
"input_state": input_state,
|
||||||
|
"content_type": content_type,
|
||||||
|
}
|
||||||
|
if content_raw:
|
||||||
|
body["content_raw"] = content_raw
|
||||||
|
if stream_msg_id:
|
||||||
|
body["stream_msg_id"] = stream_msg_id
|
||||||
|
if input_mode:
|
||||||
|
body["input_mode"] = input_mode
|
||||||
|
if msg_id:
|
||||||
|
body["msg_id"] = msg_id
|
||||||
|
elif event_id:
|
||||||
|
body["event_id"] = event_id
|
||||||
|
if msg_seq is not None:
|
||||||
|
body["msg_seq"] = msg_seq
|
||||||
|
if is_wakeup is not None:
|
||||||
|
body["is_wakeup"] = is_wakeup
|
||||||
|
body.update(extra)
|
||||||
|
return await self.post(f"/v2/users/{openid}/stream_messages", json=body)
|
||||||
|
|
||||||
async def post_group_message(
|
async def post_group_message(
|
||||||
self,
|
self,
|
||||||
group_openid: str,
|
group_openid: str,
|
||||||
@@ -144,6 +238,52 @@ class V2Api(BaseApi):
|
|||||||
f"/v2/groups/{group_openid}", file_type, url, srv_send_msg
|
f"/v2/groups/{group_openid}", file_type, url, srv_send_msg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def prepare_group_upload(
|
||||||
|
self,
|
||||||
|
group_openid: str,
|
||||||
|
file_type: int,
|
||||||
|
file_size: int | str,
|
||||||
|
file_name: str,
|
||||||
|
md5: str,
|
||||||
|
sha1: str,
|
||||||
|
md5_10m: str,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""群聊大文件分片预上传(POST /v2/groups/{group_openid}/upload_prepare)。
|
||||||
|
|
||||||
|
返回 upload_id、分块大小 block_size 与各分片预签名 URL(parts);
|
||||||
|
将文件按 block_size 分片逐片 PUT 到预签名 URL,每片成功后调
|
||||||
|
finish_group_upload_part。md5/sha1 为整文件校验值,md5_10m 为
|
||||||
|
文件前 10002432 字节(约 10MB)的 MD5。全部分片完成后携带
|
||||||
|
upload_id 调 upload_group_file 完成合并。
|
||||||
|
"""
|
||||||
|
return await self._prepare_upload(
|
||||||
|
f"/v2/groups/{group_openid}",
|
||||||
|
file_type,
|
||||||
|
file_size,
|
||||||
|
file_name,
|
||||||
|
md5,
|
||||||
|
sha1,
|
||||||
|
md5_10m,
|
||||||
|
**extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def finish_group_upload_part(
|
||||||
|
self,
|
||||||
|
group_openid: str,
|
||||||
|
upload_id: str,
|
||||||
|
part_index: int,
|
||||||
|
block_size: int | str | None = None,
|
||||||
|
md5: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""群聊分片上传完成确认(POST .../upload_part_finish)。
|
||||||
|
|
||||||
|
每个分片 PUT 到预签名 URL 成功后调用,通知服务端该分片已传完。
|
||||||
|
"""
|
||||||
|
return await self._finish_upload_part(
|
||||||
|
f"/v2/groups/{group_openid}", upload_id, part_index, block_size, md5
|
||||||
|
)
|
||||||
|
|
||||||
async def _upload_v2_file(
|
async def _upload_v2_file(
|
||||||
self, base: str, file_type: int, url: str, srv_send_msg: bool
|
self, base: str, file_type: int, url: str, srv_send_msg: bool
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@@ -152,6 +292,44 @@ class V2Api(BaseApi):
|
|||||||
json={"file_type": file_type, "url": url, "srv_send_msg": srv_send_msg},
|
json={"file_type": file_type, "url": url, "srv_send_msg": srv_send_msg},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _prepare_upload(
|
||||||
|
self,
|
||||||
|
base: str,
|
||||||
|
file_type: int,
|
||||||
|
file_size: int | str,
|
||||||
|
file_name: str,
|
||||||
|
md5: str,
|
||||||
|
sha1: str,
|
||||||
|
md5_10m: str,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
# file_size 官方为字符串类型(字节)
|
||||||
|
body: dict[str, Any] = {
|
||||||
|
"file_type": file_type,
|
||||||
|
"file_size": str(file_size),
|
||||||
|
"file_name": file_name,
|
||||||
|
"md5": md5,
|
||||||
|
"sha1": sha1,
|
||||||
|
"md5_10m": md5_10m,
|
||||||
|
}
|
||||||
|
body.update(extra)
|
||||||
|
return await self.post(f"{base}/upload_prepare", json=body)
|
||||||
|
|
||||||
|
async def _finish_upload_part(
|
||||||
|
self,
|
||||||
|
base: str,
|
||||||
|
upload_id: str,
|
||||||
|
part_index: int,
|
||||||
|
block_size: int | str | None,
|
||||||
|
md5: str | None,
|
||||||
|
) -> dict:
|
||||||
|
body: dict[str, Any] = {"upload_id": upload_id, "part_index": part_index}
|
||||||
|
if block_size is not None:
|
||||||
|
body["block_size"] = str(block_size)
|
||||||
|
if md5 is not None:
|
||||||
|
body["md5"] = md5
|
||||||
|
return await self.post(f"{base}/upload_part_finish", json=body)
|
||||||
|
|
||||||
async def _post_message(
|
async def _post_message(
|
||||||
self,
|
self,
|
||||||
path: str,
|
path: str,
|
||||||
@@ -216,3 +394,14 @@ class V2Api(BaseApi):
|
|||||||
f"/v2/groups/{group_openid}/restrict_chat_setting",
|
f"/v2/groups/{group_openid}/restrict_chat_setting",
|
||||||
json={"mute_expire_at": mute_expire_at},
|
json={"mute_expire_at": mute_expire_at},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def generate_url_link(self, callback_data: str | None = None) -> dict:
|
||||||
|
"""生成机器人分享链接(POST /v2/generate_url_link),返回 {"data": {"url": ...}}。
|
||||||
|
|
||||||
|
用于邀请用户添加机器人为好友;callback_data(≤32 字符)在用户
|
||||||
|
通过链接添加时透传给开发者。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {}
|
||||||
|
if callback_data:
|
||||||
|
body["callback_data"] = callback_data
|
||||||
|
return await self.post("/v2/generate_url_link", json=body)
|
||||||
@@ -79,6 +79,7 @@ class Intent(IntFlag):
|
|||||||
GUILD_MESSAGES = 1 << 9
|
GUILD_MESSAGES = 1 << 9
|
||||||
GUILD_MESSAGE_REACTIONS = 1 << 10
|
GUILD_MESSAGE_REACTIONS = 1 << 10
|
||||||
DIRECT_MESSAGE = 1 << 12
|
DIRECT_MESSAGE = 1 << 12
|
||||||
|
GROUP_MEMBER_EVENT = 1 << 24
|
||||||
GROUP_AND_C2C_EVENT = 1 << 25
|
GROUP_AND_C2C_EVENT = 1 << 25
|
||||||
INTERACTION = 1 << 26
|
INTERACTION = 1 << 26
|
||||||
MESSAGE_AUDIT = 1 << 27
|
MESSAGE_AUDIT = 1 << 27
|
||||||
|
|||||||
@@ -126,6 +126,56 @@ class GroupMsgReceive:
|
|||||||
event_ts: str | None = None
|
event_ts: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GroupMemberChange:
|
||||||
|
"""群成员加入/退出(GROUP_MEMBER_ADD / GROUP_MEMBER_REMOVE,d 结构一致)。
|
||||||
|
|
||||||
|
user_openid 为跨应用统一标识,可能为空。
|
||||||
|
"""
|
||||||
|
|
||||||
|
timestamp: int | None = None
|
||||||
|
group_openid: str | None = None
|
||||||
|
member_openid: str | None = None
|
||||||
|
user_openid: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GroupJoinRequest:
|
||||||
|
"""用户申请加群(GROUP_JOIN_REQUEST):仅机器人是群管理员时收到。
|
||||||
|
|
||||||
|
verify_info 为入群验证方式({"method": "verify_message"/
|
||||||
|
"admin_review_qa", "verify_message": ..., "review_qa_list": [...]});
|
||||||
|
auto_approved 非空(含 strategy_id)表示申请已被自动审批策略通过。
|
||||||
|
join_request_id 需在审批接口(review_group_join_request)回传。
|
||||||
|
"""
|
||||||
|
|
||||||
|
group_openid: str | None = None
|
||||||
|
join_request_id: str | None = None
|
||||||
|
member_openid: str | None = None
|
||||||
|
username: str | None = None
|
||||||
|
risk_tips: str | None = None
|
||||||
|
union_openid: str | None = None
|
||||||
|
apply_at: str | None = None
|
||||||
|
apply_source: str | None = None
|
||||||
|
invited_by: str | None = None
|
||||||
|
bot: bool | None = None
|
||||||
|
verify_info: dict | None = None
|
||||||
|
auto_approved: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class SubscribeMessageStatus:
|
||||||
|
"""订阅消息授权状态变更(SUBSCRIBE_MESSAGE_STATUS)。
|
||||||
|
|
||||||
|
result 为各模板授权结果列表:op 1=允许订阅 2=拒绝订阅,
|
||||||
|
subscribe_id 发送订阅消息时需使用;group_openid/openid 按场景二选一。
|
||||||
|
"""
|
||||||
|
|
||||||
|
group_openid: str | None = None
|
||||||
|
openid: str | None = None
|
||||||
|
result: list[dict] | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class AtMessage:
|
class AtMessage:
|
||||||
"""频道 @机器人消息(AT_MESSAGE_CREATE / PUBLIC 与私域共用结构)。"""
|
"""频道 @机器人消息(AT_MESSAGE_CREATE / PUBLIC 与私域共用结构)。"""
|
||||||
@@ -298,6 +348,10 @@ EVENT_TYPES: dict[str, type] = {
|
|||||||
"C2C_MSG_RECEIVE": C2CMsgReceive,
|
"C2C_MSG_RECEIVE": C2CMsgReceive,
|
||||||
"GROUP_MSG_REJECT": GroupMsgReject,
|
"GROUP_MSG_REJECT": GroupMsgReject,
|
||||||
"GROUP_MSG_RECEIVE": GroupMsgReceive,
|
"GROUP_MSG_RECEIVE": GroupMsgReceive,
|
||||||
|
"GROUP_MEMBER_ADD": GroupMemberChange,
|
||||||
|
"GROUP_MEMBER_REMOVE": GroupMemberChange,
|
||||||
|
"GROUP_JOIN_REQUEST": GroupJoinRequest,
|
||||||
|
"SUBSCRIBE_MESSAGE_STATUS": SubscribeMessageStatus,
|
||||||
"AT_MESSAGE_CREATE": AtMessage,
|
"AT_MESSAGE_CREATE": AtMessage,
|
||||||
"DIRECT_MESSAGE_CREATE": DirectMessage,
|
"DIRECT_MESSAGE_CREATE": DirectMessage,
|
||||||
"GUILD_CREATE": Guild,
|
"GUILD_CREATE": Guild,
|
||||||
|
|||||||
@@ -172,6 +172,294 @@ async def test_respond_interaction():
|
|||||||
assert kwargs["json"] == {"code": 0}
|
assert kwargs["json"] == {"code": 0}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_panel_create_list_detail():
|
||||||
|
api, cap = make_api()
|
||||||
|
panel = {"items": [{"type": "command", "name": "群签到", "desc": "每日签到"}]}
|
||||||
|
await api.create_panel(
|
||||||
|
"group", panel, target_type="specific", group_openids=["G1", "G2"]
|
||||||
|
)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/panels")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"scope": "group",
|
||||||
|
"panel": panel,
|
||||||
|
"target_type": "specific",
|
||||||
|
"group_openids": ["G1", "G2"],
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.get_panels("c2c", cursor="c1", limit=10)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("GET", "/v2/panels")
|
||||||
|
assert kwargs["params"] == {"scope": "c2c", "cursor": "c1", "limit": 10}
|
||||||
|
|
||||||
|
await api.get_panel("P1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/v2/panels/P1")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_panel_update_delete_targets():
|
||||||
|
api, cap = make_api()
|
||||||
|
panel = {"items": [{"type": "command", "name": "新指令", "desc": "更新"}]}
|
||||||
|
await api.update_panel("P1", panel)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("PUT", "/v2/panels/P1")
|
||||||
|
assert kwargs["json"] == {"panel": panel}
|
||||||
|
|
||||||
|
await api.delete_panel("P1")
|
||||||
|
assert cap["call"][:2] == ("DELETE", "/v2/panels/P1")
|
||||||
|
|
||||||
|
await api.update_panel_targets("P1", "add", group_openids=["G3"])
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("PUT", "/v2/panels/P1/target")
|
||||||
|
assert kwargs["json"] == {"op": "add", "group_openids": ["G3"]}
|
||||||
|
|
||||||
|
await api.update_panel_targets("P1", "del", user_openids=["U1"])
|
||||||
|
assert cap["call"][2]["json"] == {"op": "del", "user_openids": ["U1"]}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_menu_get_and_update():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.get_menu()
|
||||||
|
assert cap["call"][:2] == ("GET", "/v2/menu")
|
||||||
|
|
||||||
|
menu = {
|
||||||
|
"items": [
|
||||||
|
{"type": "send_message", "name": "帮助", "send_message": "/help"},
|
||||||
|
{"type": "link", "name": "官网", "link": "https://example.com"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
await api.update_menu(menu)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("PUT", "/v2/menu")
|
||||||
|
assert kwargs["json"] == {"menu": menu}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- v2 补充:流式消息 / 分片上传 / 分享链接 ----------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_stream_message_first_and_next_chunk():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.post_c2c_stream_message("U1", "回答中", msg_id="m1", msg_seq=1)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/users/U1/stream_messages")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"index": 0,
|
||||||
|
"input_state": 1,
|
||||||
|
"content_type": "markdown",
|
||||||
|
"content_raw": "回答中",
|
||||||
|
"msg_id": "m1",
|
||||||
|
"msg_seq": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.post_c2c_stream_message(
|
||||||
|
"U1",
|
||||||
|
"回答中,更多内容",
|
||||||
|
stream_msg_id="s1",
|
||||||
|
index=1,
|
||||||
|
input_state=10,
|
||||||
|
input_mode="replace",
|
||||||
|
event_id="e1",
|
||||||
|
)
|
||||||
|
body = cap["call"][2]["json"]
|
||||||
|
assert body["stream_msg_id"] == "s1"
|
||||||
|
assert body["index"] == 1
|
||||||
|
assert body["input_state"] == 10
|
||||||
|
assert body["input_mode"] == "replace"
|
||||||
|
assert body["event_id"] == "e1"
|
||||||
|
assert "msg_id" not in body
|
||||||
|
|
||||||
|
|
||||||
|
async def test_chunked_upload_group_and_c2c():
|
||||||
|
api, cap = make_api()
|
||||||
|
common = {
|
||||||
|
"file_type": 2,
|
||||||
|
"file_size": 31457280,
|
||||||
|
"file_name": "demo.mp4",
|
||||||
|
"md5": "m",
|
||||||
|
"sha1": "s",
|
||||||
|
"md5_10m": "m10",
|
||||||
|
}
|
||||||
|
await api.prepare_group_upload("G1", **common)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/groups/G1/upload_prepare")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"file_type": 2,
|
||||||
|
"file_size": "31457280", # 官方为字符串类型
|
||||||
|
"file_name": "demo.mp4",
|
||||||
|
"md5": "m",
|
||||||
|
"sha1": "s",
|
||||||
|
"md5_10m": "m10",
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.finish_group_upload_part("G1", "up1", part_index=0)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/groups/G1/upload_part_finish")
|
||||||
|
assert kwargs["json"] == {"upload_id": "up1", "part_index": 0}
|
||||||
|
|
||||||
|
await api.prepare_c2c_upload("U1", **common)
|
||||||
|
assert cap["call"][:2] == ("POST", "/v2/users/U1/upload_prepare")
|
||||||
|
|
||||||
|
await api.finish_c2c_upload_part("U1", "up1", part_index=1, block_size=4096, md5="p1")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/users/U1/upload_part_finish")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"upload_id": "up1",
|
||||||
|
"part_index": 1,
|
||||||
|
"block_size": "4096",
|
||||||
|
"md5": "p1",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_generate_url_link():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.generate_url_link()
|
||||||
|
assert cap["call"][2]["json"] == {}
|
||||||
|
|
||||||
|
await api.generate_url_link("track_01")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/generate_url_link")
|
||||||
|
assert kwargs["json"] == {"callback_data": "track_01"}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 频道补充:子频道增删改 / 机器人频道列表 ----------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_channel_manage_and_me_guilds():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.create_channel("GID", "公告区", type=0, sub_type=1, position=3)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/guilds/GID/channels")
|
||||||
|
assert kwargs["json"] == {"name": "公告区", "type": 0, "sub_type": 1, "position": 3}
|
||||||
|
|
||||||
|
await api.update_channel("C1", name="新名字", position=5)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("PATCH", "/channels/C1")
|
||||||
|
assert kwargs["json"] == {"name": "新名字", "position": 5}
|
||||||
|
|
||||||
|
await api.delete_channel("C1")
|
||||||
|
assert cap["call"][:2] == ("DELETE", "/channels/C1")
|
||||||
|
|
||||||
|
await api.get_me_guilds(after="G9", limit=20)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("GET", "/users/@me/guilds")
|
||||||
|
assert kwargs["params"] == {"after": "G9", "limit": 20}
|
||||||
|
|
||||||
|
await api.get_me_guilds()
|
||||||
|
assert cap["call"][2].get("params") is None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 群管理 ----------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_group_info_members_state():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.get_group_info("G1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/v2/groups/G1/info")
|
||||||
|
|
||||||
|
await api.get_group_members("G1", cursor="c1")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("GET", "/v2/groups/G1/members")
|
||||||
|
assert kwargs["params"] == {"cursor": "c1"}
|
||||||
|
|
||||||
|
await api.get_group_member("G1", "M1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/v2/groups/G1/members/M1")
|
||||||
|
|
||||||
|
await api.get_group_bot_state("G1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/v2/groups/G1/bot_state")
|
||||||
|
|
||||||
|
await api.get_group_mute_state("G1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/v2/groups/G1/restrict_chat_setting")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_group_join_request_flow():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.get_group_join_requests("G1", limit=10)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("GET", "/v2/groups/G1/join_request_list")
|
||||||
|
assert kwargs["params"] == {"limit": 10}
|
||||||
|
|
||||||
|
await api.review_group_join_request(
|
||||||
|
"G1", "M1", "approve", join_request_id="j1"
|
||||||
|
)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/groups/G1/approval_join_request/M1")
|
||||||
|
assert kwargs["json"] == {"op": "approve", "join_request_id": "j1"}
|
||||||
|
|
||||||
|
await api.review_group_join_request(
|
||||||
|
"G1", "M1", "decline", join_request_id="j1",
|
||||||
|
reject_reason="广告", add_to_member_blacklist=True,
|
||||||
|
)
|
||||||
|
body = cap["call"][2]["json"]
|
||||||
|
assert body == {
|
||||||
|
"op": "decline",
|
||||||
|
"join_request_id": "j1",
|
||||||
|
"reject_reason": "广告",
|
||||||
|
"add_to_member_blacklist": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_group_blacklist_and_remove():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.get_group_blacklist("G1", cursor="c1", limit=50)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("GET", "/v2/groups/G1/member_blacklist")
|
||||||
|
assert kwargs["params"] == {"cursor": "c1", "limit": 50}
|
||||||
|
|
||||||
|
await api.update_group_blacklist("G1", "add", ["M1", "M2"])
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/groups/G1/member_blacklist")
|
||||||
|
assert kwargs["json"] == {"op": "add", "member_openids": ["M1", "M2"]}
|
||||||
|
|
||||||
|
await api.remove_group_members("G1", ["M1"], add_to_member_blacklist=True)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/groups/G1/batch_remove_members")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"member_openids": ["M1"],
|
||||||
|
"add_to_member_blacklist": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_join_approval_strategies():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.get_join_approval_strategies(cursor="c1", limit=20)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("GET", "/v2/groups/join_approval_strategy")
|
||||||
|
assert kwargs["params"] == {"cursor": "c1", "limit": 20}
|
||||||
|
|
||||||
|
await api.create_join_approval_strategy(
|
||||||
|
group_openids=["G1"], expire_at="2027-08-05T15:30:16+08:00"
|
||||||
|
)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/groups/join_approval_strategy")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"group_openids": ["G1"],
|
||||||
|
"expire_at": "2027-08-05T15:30:16+08:00",
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.update_join_approval_strategy(
|
||||||
|
"st1", is_enable="off", group_action={"op": "add", "group_openids": ["G2"]}
|
||||||
|
)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("PATCH", "/v2/groups/join_approval_strategy/st1")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"is_enable": "off",
|
||||||
|
"group_action": {"op": "add", "group_openids": ["G2"]},
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.delete_join_approval_strategy("st1")
|
||||||
|
assert cap["call"][:2] == ("DELETE", "/v2/groups/join_approval_strategy/st1")
|
||||||
|
|
||||||
|
await api.execute_join_approval_strategy("st1")
|
||||||
|
assert cap["call"][:2] == ("POST", "/v2/groups/join_approval_strategy/st1/execute")
|
||||||
|
|
||||||
|
await api.update_join_approval_strategy_whitelist("st1", "add", ["1234567"])
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == (
|
||||||
|
"POST",
|
||||||
|
"/v2/groups/join_approval_strategy/st1/whitelist_users",
|
||||||
|
)
|
||||||
|
assert kwargs["json"] == {"op": "add", "whitelist_users": ["1234567"]}
|
||||||
|
|
||||||
|
|
||||||
def test_button_defaults_and_overrides():
|
def test_button_defaults_and_overrides():
|
||||||
btn = button("点我", data="d1")
|
btn = button("点我", data="d1")
|
||||||
assert btn["render_data"] == {"label": "点我", "style": 1}
|
assert btn["render_data"] == {"label": "点我", "style": 1}
|
||||||
@@ -369,3 +657,137 @@ async def test_request_401_invalidates_token_and_retries_once():
|
|||||||
assert token.invalidated == 1
|
assert token.invalidated == 1
|
||||||
assert token.fetches == 2
|
assert token.fetches == 2
|
||||||
assert len(http.calls) == 2
|
assert len(http.calls) == 2
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 旧版(频道侧)接口补齐 ----------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_guild_roles_crud_and_member_role():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.create_guild_role("GID", "码农", color=0xFF0000, hoist=1)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/guilds/GID/roles")
|
||||||
|
assert kwargs["json"] == {"name": "码农", "color": 16711680, "hoist": 1}
|
||||||
|
|
||||||
|
await api.update_guild_role("GID", "R1", name="新名")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("PATCH", "/guilds/GID/roles/R1")
|
||||||
|
assert kwargs["json"] == {"name": "新名"}
|
||||||
|
|
||||||
|
await api.delete_guild_role("GID", "R1")
|
||||||
|
assert cap["call"][:2] == ("DELETE", "/guilds/GID/roles/R1")
|
||||||
|
|
||||||
|
await api.add_guild_member_role("GID", "U1", "R1")
|
||||||
|
assert cap["call"][:2] == ("PUT", "/guilds/GID/members/U1/roles/R1")
|
||||||
|
|
||||||
|
await api.remove_guild_member_role("GID", "U1", "R1")
|
||||||
|
assert cap["call"][:2] == ("DELETE", "/guilds/GID/members/U1/roles/R1")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_guild_members_mutes_and_api_permission():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.get_guild_members("GID")
|
||||||
|
assert cap["call"][:2] == ("GET", "/guilds/GID/members")
|
||||||
|
|
||||||
|
await api.remove_guild_member("GID", "U1", add_blacklist=True, delete_history_msg_days=7)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("DELETE", "/guilds/GID/members/U1")
|
||||||
|
assert kwargs["json"] == {"add_blacklist": True, "delete_history_msg_days": 7}
|
||||||
|
|
||||||
|
await api.get_role_members("GID", "R1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/guilds/GID/roles/R1/members")
|
||||||
|
|
||||||
|
await api.get_channel_online_nums("C1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/channels/C1/online_nums")
|
||||||
|
|
||||||
|
await api.mute_guild("GID", mute_seconds="60")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("PATCH", "/guilds/GID/mute")
|
||||||
|
assert kwargs["json"] == {"mute_seconds": "60"}
|
||||||
|
|
||||||
|
await api.mute_guild_members("GID", ["U1", "U2"], mute_end_timestamp="1700000000")
|
||||||
|
assert cap["call"][2]["json"] == {
|
||||||
|
"user_ids": ["U1", "U2"],
|
||||||
|
"mute_end_timestamp": "1700000000",
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.get_guild_message_setting("GID")
|
||||||
|
assert cap["call"][:2] == ("GET", "/guilds/GID/message/setting")
|
||||||
|
|
||||||
|
await api.get_guild_api_permissions("GID")
|
||||||
|
assert cap["call"][:2] == ("GET", "/guilds/GID/api_permission")
|
||||||
|
|
||||||
|
await api.create_api_permission_demand("GID", "C1", "/channels/x/messages", "POST", "d")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/guilds/GID/api_permission/demand")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"channel_id": "C1",
|
||||||
|
"api_identify": {"path": "/channels/x/messages", "method": "POST"},
|
||||||
|
"desc": "d",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_channel_permissions_and_dms():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.get_channel_permissions("C1", "U1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/channels/C1/members/U1/permissions")
|
||||||
|
|
||||||
|
await api.get_channel_role_permissions("C1", "R1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/channels/C1/roles/R1/permissions")
|
||||||
|
|
||||||
|
await api.update_channel_permissions("C1", "U1", add="1", remove="4")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("PUT", "/channels/C1/members/U1/permissions")
|
||||||
|
assert kwargs["json"] == {"add": "1", "remove": "4"}
|
||||||
|
|
||||||
|
await api.update_channel_role_permissions("C1", "R1", add="2")
|
||||||
|
assert cap["call"][2]["json"] == {"add": "2"}
|
||||||
|
|
||||||
|
await api.create_dms("U1", "GID")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/users/@me/dms")
|
||||||
|
assert kwargs["json"] == {"recipient_id": "U1", "source_guild_id": "GID"}
|
||||||
|
|
||||||
|
await api.post_dms_message("DG", content="hi", msg_id="m1", msg_seq=2)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/dms/DG/messages")
|
||||||
|
assert kwargs["json"] == {"msg_seq": 2, "content": "hi", "msg_id": "m1"}
|
||||||
|
|
||||||
|
await api.withdraw_dms_message("DG", "m1", hide_tip=True)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("DELETE", "/dms/DG/messages/m1")
|
||||||
|
assert kwargs["params"] == {"hidetip": "true"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_audio_and_forum():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.control_channel_audio("C1", audio_url="http://x/a.mp3", text="歌名")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/channels/C1/audio")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"status": 0,
|
||||||
|
"audio_url": "http://x/a.mp3",
|
||||||
|
"text": "歌名",
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.control_channel_audio("C1", status=3)
|
||||||
|
assert cap["call"][2]["json"] == {"status": 3}
|
||||||
|
|
||||||
|
await api.put_channel_mic("C1")
|
||||||
|
assert cap["call"][:2] == ("PUT", "/channels/C1/mic")
|
||||||
|
await api.delete_channel_mic("C1")
|
||||||
|
assert cap["call"][:2] == ("DELETE", "/channels/C1/mic")
|
||||||
|
|
||||||
|
await api.get_channel_threads("C1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/channels/C1/threads")
|
||||||
|
|
||||||
|
await api.get_channel_thread("C1", "T1")
|
||||||
|
assert cap["call"][:2] == ("GET", "/channels/C1/threads/T1")
|
||||||
|
|
||||||
|
await api.put_channel_thread("C1", "标题", "内容", format=3)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("PUT", "/channels/C1/threads")
|
||||||
|
assert kwargs["json"] == {"title": "标题", "content": "内容", "format": 3}
|
||||||
|
|
||||||
|
await api.delete_channel_thread("C1", "T1")
|
||||||
|
assert cap["call"][:2] == ("DELETE", "/channels/C1/threads/T1")
|
||||||
@@ -192,3 +192,59 @@ async def test_untyped_annotation_still_receives_raw_dict():
|
|||||||
{"op": Opcode.DISPATCH, "t": "GROUP_AT_MESSAGE_CREATE", "d": {"content": "x"}}
|
{"op": Opcode.DISPATCH, "t": "GROUP_AT_MESSAGE_CREATE", "d": {"content": "x"}}
|
||||||
)
|
)
|
||||||
assert received == [{"content": "x"}]
|
assert received == [{"content": "x"}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_group_member_and_join_request_events():
|
||||||
|
from qqbotsdk.payloads import (
|
||||||
|
GroupJoinRequest,
|
||||||
|
GroupMemberChange,
|
||||||
|
SubscribeMessageStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
change = parse_event(
|
||||||
|
"GROUP_MEMBER_ADD",
|
||||||
|
{
|
||||||
|
"timestamp": 1784276757,
|
||||||
|
"group_openid": "G1",
|
||||||
|
"member_openid": "M1",
|
||||||
|
"user_openid": "U1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert isinstance(change, GroupMemberChange)
|
||||||
|
assert change.member_openid == "M1"
|
||||||
|
# 两事件 d 结构一致,共用 dataclass
|
||||||
|
remove = parse_event(
|
||||||
|
"GROUP_MEMBER_REMOVE",
|
||||||
|
{"timestamp": 1, "group_openid": "G1", "member_openid": "M2"},
|
||||||
|
)
|
||||||
|
assert isinstance(remove, GroupMemberChange)
|
||||||
|
assert remove.user_openid is None
|
||||||
|
|
||||||
|
req = parse_event(
|
||||||
|
"GROUP_JOIN_REQUEST",
|
||||||
|
{
|
||||||
|
"group_openid": "G1",
|
||||||
|
"join_request_id": "j1",
|
||||||
|
"member_openid": "M1",
|
||||||
|
"username": "小明",
|
||||||
|
"apply_source": "self_apply",
|
||||||
|
"verify_info": {"method": "verify_message", "verify_message": "来了"},
|
||||||
|
"auto_approved": {"strategy_id": "st1"},
|
||||||
|
"unknown_field": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert isinstance(req, GroupJoinRequest)
|
||||||
|
assert req.join_request_id == "j1"
|
||||||
|
assert req.verify_info == {"method": "verify_message", "verify_message": "来了"}
|
||||||
|
assert req.auto_approved == {"strategy_id": "st1"}
|
||||||
|
|
||||||
|
status = parse_event(
|
||||||
|
"SUBSCRIBE_MESSAGE_STATUS",
|
||||||
|
{
|
||||||
|
"openid": "U1",
|
||||||
|
"result": [{"template_id": 10001, "op": 1, "subscribe_id": "sub1"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert isinstance(status, SubscribeMessageStatus)
|
||||||
|
assert status.group_openid is None
|
||||||
|
assert status.result[0]["subscribe_id"] == "sub1"
|
||||||
Reference in new issue
Block a user