├── .gitignore ├── LICENSE ├── README.md ├── bot_api ├── __init__.py ├── api.py ├── inter.py ├── logger.py ├── models.py ├── sdk_main.py ├── structs.py └── utils.py ├── bot_main.py ├── config.example.yaml ├── main.py ├── requirements.txt └── server ├── __init__.py ├── message_convert.py ├── sender.py ├── server.py └── tools.py /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。 3 | ################################################################################ 4 | 5 | /.idea 6 | /.vs 7 | /bot_api/__pycache__ 8 | /server/__pycache__ 9 | /chieri_bot 10 | /__pycache__ 11 | /server/temp 12 | /bot_api/log 13 | /ycm 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 chinosk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QQ官方频道机器人SDK 2 | 3 | - 开发中~ 4 | 5 | 6 | 7 | # 直接使用SDK 8 | 9 | - 参考 `bot_main.py` 10 | - 在 `bot_main.py` 同级目录下创建名为 `config.yaml` 的配置文件,填入自己的 `bot_app_id` 、 `bot_token` 及 `bot_secret`,内容类似下面所示,也可直接使用仓库里的 `config.example.yaml` 文件,然后自己修改后缀名和内容 11 | ```yaml 12 | bot: 13 | id: 123 # 机器人id 14 | token: 456 # 机器人token 15 | secret: 789 # 机器人secret 16 | ``` 17 | ```python 18 | import bot_api 19 | from bot_api.models import Ark, Embed 20 | from bot_api.utils import yaml_util 21 | 22 | token = yaml_util.read('config.yaml') 23 | bot = bot_api.BotApp(token['bot']['id'], token['bot']['token'], token['bot']['secret'], 24 | is_sandbox=True, debug=True, api_return_pydantic=True, 25 | ignore_at_self=False, # 过滤消息正文中艾特Bot自身的内容, 默认为False 26 | inters=[bot_api.Intents.GUILDS, bot_api.Intents.AT_MESSAGES, 27 | bot_api.Intents.GUILD_MEMBERS]) # 事件订阅 28 | 29 | 30 | @bot.receiver(bot_api.structs.Codes.SeverCode.BotGroupAtMessage) # 填入对应的参数实现处理对应事件 31 | def get_at_message(chain: bot_api.structs.Message): # 注册一个艾特消息处理器 32 | bot.logger(f"收到来自频道:{chain.guild_id} 子频道: {chain.channel_id} " 33 | f"内用户: {chain.author.username}({chain.author.id}) 的消息: {chain.content} ({chain.id})") 34 | 35 | if "你好" in chain.content: 36 | bot.api_send_message(chain.channel_id, chain.id, "hello world!") 37 | elif "test" in chain.content: 38 | bot.api_send_message(chain.channel_id, chain.id, "chieri在哟~") 39 | elif "/echo" in chain.content: 40 | reply = chain.content[chain.content.find("/echo") + len("/echo"):].strip() 41 | bot.api_send_message(chain.channel_id, chain.id, reply) 42 | 43 | elif "/embed" in chain.content: # 发送embed 44 | send_embed = Embed("标题", ["文本1", "文本2", "文本3"], image_url=None) 45 | bot.api_send_message(chain.channel_id, chain.id, embed=send_embed) 46 | 47 | elif "/ark" in chain.content: # 发送ark消息, 需要Ark权限 48 | send_ark = Ark.LinkWithText("描述", "提示信息", [["纯文本1"], ["纯文本2"], ["链接文本1", "http://baidu.com"]]) 49 | bot.api_send_message(chain.channel_id, chain.id, ark=send_ark) 50 | 51 | 52 | bot.start() # 启动bot 53 | ``` 54 | 55 | 56 | 57 | 58 | 59 | 60 | ## 关于部分事件官方已改为被动消息说明 61 | 62 | | 事件代号 | 事件描述 | 63 | | :----------------------: | :----------------------: | 64 | | GUILD_MEMBER_ADD |成员加入| 65 | | GUILD_MEMBER_UPDATE |成员资料变更| 66 | | GUILD_MEMBER_REMOVE |成员被移除| 67 | | MESSAGE_REACTION_ADD |消息添加表情表态| 68 | | MESSAGE_REACTION_REMOVE |消息删除表情表态| 69 | | FORUM_THREAD_CREATE |用户创建主题| 70 | | FORUM_THREAD_UPDATE |用户更新主题| 71 | | FORUM_THREAD_DELETE |用户删除主题| 72 | | FORUM_POST_CREATE |用户创建帖子| 73 | | FORUM_POST_DELETE |用户删除帖子| 74 | | FORUM_REPLY_CREATE |用户回复评论| 75 | | FORUM_REPLY_DELETE |用户回复评论| 76 | 77 | ### 使用:即将要回复的消息ID(msg_id)替换为以上事件代号即可 78 | ```python 79 | 这里以添加表情表态为例: 80 | 81 | bot.api_send_message(channel_id, msg_id='MESSAGE_REACTION_ADD', content='ok') 82 | ``` 83 | 84 | 85 | 86 | 87 | 88 | ### 自行组合消息 89 | 90 | `api_send_message`方法提供了极高的自由度。您可以按照本文档提供的方法发送消息, 也可以使用`others_parameter`参数自行组合。 91 | 92 | 使用`others_parameter`, 您需要按照 [QQ机器人文档 - 发送消息](https://bot.q.qq.com/wiki/develop/api/openapi/message/post_messages.html) 提供的参数发送消息, 若有此SDK没有支持的消息类型, 您依旧可以自行组合参数进行发送。 93 | 94 | 下面是发送一条文本+图片消息的例子 95 | 96 | ----- 97 | 98 | - 一般情况下, 您可以: 99 | 100 | ```python 101 | bot.api_send_message(chain.channel_id, chain.id, "这是消息", "http://您的图片") 102 | ``` 103 | 104 | - 您也可以: 105 | 106 | ```python 107 | bot.api_send_message(chain.channel_id, chain.id, others_parameter={"content": "这是消息", "image": "https://您的图片"}) 108 | ``` 109 | 110 | 111 | 112 | 113 | 114 | 115 | ## Ark消息说明 116 | 117 | - 注意: 发送Ark消息需要向官方申请Ark权限, 否则无法发送 118 | 119 | ### 引用: 120 | 121 | ```python 122 | from bot_api.models import Ark, Embed 123 | ``` 124 | 125 | ### 发送[Embed](https://bot.q.qq.com/wiki/develop/api/openapi/message/template/embed_message.html)消息 126 | 127 | ```python 128 | send_embed = Embed("标题", ["文本1", "文本2", "文本3"], image_url="http://你的图片") 129 | "image_url" 130 | 参数可选, 若没有图片, 则不填 131 | 132 | bot.api_send_message(channel_id, message_id, embed=send_embed) 133 | ``` 134 | 135 | ### 发送[Ark](https://bot.q.qq.com/wiki/develop/api/openapi/message/message_template.html)消息 136 | 137 | - `Ark`类中目前有`LinkWithText`, `TextAndThumbnail`, `BigImage`三个子类, 分别对应 [23 链接+文本列表模板](https://bot.q.qq.com/wiki/develop/api/openapi/message/template/template_23.html), [24 文本+缩略图模板](https://bot.q.qq.com/wiki/develop/api/openapi/message/template/template_24.html), [37 大图模板](https://bot.q.qq.com/wiki/develop/api/openapi/message/template/template_37.html), 下面以构造相对复杂的 [23 链接+文本列表模板 ](https://bot.q.qq.com/wiki/develop/api/openapi/message/template/template_23.html)为例 138 | 139 | ```python 140 | send_ark = Ark.LinkWithText("描述", "提示信息", 141 | [["纯文本1"], ["纯文本2"], ["链接文本1", "http://baidu.com"], ["链接文本2", "http://google.com"]]) 142 | 143 | bot.api_send_message(channel_id, message_id, ark=send_ark) 144 | ``` 145 | 146 | 147 | 148 | 149 | 150 | ## 目前支持的事件/API 151 | 152 | #### 事件 153 | 154 | - 下表中所有事件触发时都会在新线程中执行 155 | 156 | - `事件代号`: 注册函数时, 输入对应事件代号, 在触发相应事件时, 所有被注册函数将被调用。 157 | 158 | - 位于: 类`bot_api.structs.Codes.SeverCode`, 继承自`GatewayEventName` 159 | 160 | - `传入参数`指被注册函数的参数 161 | - 位于: 类`bot_api.structs` 162 | 163 | | 事件代号 | 传入参数 | 事件描述 | 164 | | :----------------------: | :----------------------: | :-----------------------------------: | 165 | | FUNC_CALL_AFTER_BOT_LOAD | 初始化后的BotAPP类(self) | 当Bot初始化完成后, 会立刻执行这些函数 | 166 | | AT_MESSAGE_CREATE | Message | 收到艾特消息 | 167 | | MESSAGE_CREATE | Message | 收到消息(仅私域机器人可用) | 168 | | DIRECT_MESSAGE_CREATE | Message | 收到私聊消息 | 169 | | GUILD_CREATE | Guild | bot加入频道 | 170 | | GUILD_UPDATE | Guild | 频道信息更新 | 171 | | GUILD_DELETE | Guild | 频道解散/bot被移除 | 172 | | CHANNEL_CREATE | Channel | 子频道被创建 | 173 | | CHANNEL_UPDATE | Channel | 子频道信息变更 | 174 | | CHANNEL_DELETE | Channel | 子频道被删除 | 175 | | GUILD_MEMBER_ADD | MemberWithGuildID | 新用户加入频道 | 176 | | GUILD_MEMBER_UPDATE | - | TX: 暂无 | 177 | | GUILD_MEMBER_REMOVE | MemberWithGuildID | 用户离开频道 | 178 | | AUDIO_START | AudioAction | 音频开始播放 | 179 | | AUDIO_FINISH | AudioAction | 音频结束 | 180 | | AUDIO_ON_MIC | AudioAction | 上麦 | 181 | | AUDIO_OFF_MIC | AudioAction | 下麦 | 182 | | MESSAGE_REACTION_ADD | MessageReaction | 添加表情表态 | 183 | | MESSAGE_REACTION_REMOVE | MessageReaction | 删除表情表态 | 184 | | THREAD_CREATE | 暂不支持 | 用户创建主题 | 185 | | THREAD_UPDATE | 暂不支持 | 用户更新主题 | 186 | | THREAD_DELETE | 暂不支持 | 用户删除主题 | 187 | | POST_CREATE | 暂不支持 | 用户创建帖子 | 188 | | POST_DELETE | 暂不支持 | 用户删除帖子 | 189 | | REPLY_CREATE | 暂不支持 | 用户回复评论 | 190 | | REPLY_DELETE | 暂不支持 | 用户回复评论 | 191 | | MESSAGE_AUDIT_PASS | MessageAudited | 消息审核通过 | 192 | | MESSAGE_AUDIT_REJECT | MessageAudited | 消息审核不通过 | 193 | | PUBLIC_MESSAGE_DELETE | MessageDelete | 消息撤回(公域) | 194 | | MESSAGE_DELETE | MessageDelete | 消息撤回(私域) | 195 | | DIRECT_MESSAGE_DELETE | MessageDelete | 消息撤回(私聊) | 196 | 197 | 198 | 199 | - 例: 注册一个`添加表情表态`处理函数 200 | 201 | ```python 202 | @bot.receiver(bot_api.structs.Codes.SeverCode.MESSAGE_REACTION_ADD) # 填入事件代号 203 | def get_at_message(event: bot_api.structs.MessageReaction): # 函数参数类型为上表对应的传入参数 204 | pass 205 | ``` 206 | 207 | 208 | 209 | 210 | ------ 211 | 212 | #### API 213 | 214 | - 初始化Bot实例后, 输入`bot.api_`, 即可根据代码补全进行使用 215 | 216 | ```python 217 | api_send_message() # 发送频道消息 218 | api_create_dms() # 创建私信会话 219 | api_send_private_message() # 发送私聊消息 220 | api_reply_message() # 回复消息(频道/私聊) 221 | api_mute_guild() # 全频道禁言 222 | api_mute_member() # 指定用户禁言 223 | api_get_self_guilds() # 获取Bot加入的频道列表 224 | api_get_self_info() # 获取Bot自身信息 225 | api_get_message() # 获取指定消息 226 | api_get_guild_channel_list() # 获取频道内子频道列表 227 | api_get_channel_info() # 获取子频道信息 228 | api_get_guild_user_info() # 获取频道用户信息 229 | api_get_guild_info() # 获取频道信息 230 | api_get_schedule_list() # 获取子频道日程列表 231 | api_get_schedule() # 获取单个日程信息 232 | api_schedule_create() # 创建日程 233 | api_schedule_change() # 修改日程 234 | api_schedule_delete() # 删除日程 235 | api_message_recall() # 撤回消息 236 | api_guild_roles_list_get() # 获取频道身份组列表 237 | api_guild_role_create() # 创建频道身份组 238 | api_guild_role_change() # 修改频道身份组 239 | api_guild_role_remove() # 删除频道身份组 240 | api_guild_role_member_add() # 增加频道身份组成员 241 | api_guild_role_member_remove() # 移除频道身份组成员 242 | api_announces_create() # 创建频道公告 243 | api_announces_remove() # 删除频道公告 244 | api_permissions_get_channel() # 获取指定子频道的权限 245 | api_permissions_change_channel() # 修改指定子频道的权限 246 | api_permissions_get_channel_group() # 获取指定子频道身份组的权限 247 | api_permissions_change_channel_group() # 修改指定子频道身份组的权限 248 | api_audio_control() # 音频控制 249 | api_get_api_permission() # 获取频道可用权限列表 250 | api_demand_api_permission() # 创建频道 API 接口权限授权链接 251 | api_add_pins() # 添加精华消息 252 | api_remove_pins() # 删除精华消息 253 | api_get_pins() # 获取精华消息 254 | api_send_message_reactions() # 发送表情表态 255 | api_send_message_guide() # 发送消息设置引导 256 | api_get_guild_message_freq() # 获取频道消息频率设置 257 | 258 | api_pv_get_member_list() # 仅私域机器人可用 - 取频道成员列表 259 | api_pv_kick_member() # 仅私域机器人可用 - 踢出指定成员 260 | api_pv_create_channel() # 仅私域机器人可用 - 创建子频道 261 | api_pv_change_channel() # 仅私域机器人可用 - 修改子频道信息 262 | api_pv_delete_channel() # 仅私域机器人可用 - 删除子频道 263 | ``` 264 | 265 | ------ 266 | 267 |
268 | 269 |
270 | 271 |
272 | 273 | # 作为HTTP API使用 274 | 275 | - 一个人同时更新两套SDK显然是非常困难的。~~因此HTTP API部分的更新进度会慢于SDK本体~~(已停更)。欢迎有能之士前来Pr~ 276 | 277 | - 参考`main.py` 278 | 279 | ```python 280 | import bot_api 281 | import server 282 | 283 | bot = bot_api.BotApp(123456, "你的bot token", "你的bot secret", 284 | is_sandbox=True, debug=True, api_return_pydantic=True, 285 | inters=[bot_api.Intents.GUILDS, bot_api.Intents.AT_MESSAGES, bot_api.Intents.GUILD_MEMBERS]) # 事件订阅 286 | 287 | 288 | app = server.BotServer(bot, ip_call="127.0.0.1", port_call=11415, ip_listen="127.0.0.1", port_listen=1988, 289 | allow_push=False) 290 | 291 | # 开始注册事件, 可以选择需要的进行注册 292 | app.reg_bot_at_message() # 艾特消息事件 293 | app.reg_guild_member_add() # 成员增加事件 294 | app.reg_guild_member_remove() # 成员减少事件 295 | 296 | # 以下事件与onebot差别较大 297 | app.reg_guild_create() # Bot加入频道事件 298 | app.reg_guild_update() # 频道信息更新事件 299 | app.reg_guild_delete() # Bot离开频道/频道被解散事件 300 | app.reg_channel_create() # 子频道创建事件 301 | app.reg_channel_update() # 子频道信息更新事件 302 | app.reg_channel_delete() # 子频道删除事件 303 | 304 | @app.bot.receiver(bot_api.structs.Codes.SeverCode.image_to_url) # 注册一个图片转url方法 305 | def img_to_url(img_path: str): 306 | # 用处: 发送图片时, 使用图片cq码[CQ:image,file=]或[CQ:image,url=] 307 | # 装饰器作用为: 解析cq码中图片的路径(网络图片则下载到本地), 将绝对路径传给本函数, 自行操作后, 返回图片url, sdk将使用此url发送图片 308 | # 若不注册此方法, 则无法发送图片。 309 | print(img_path) 310 | return "https://你的图片url" 311 | 312 | # 注册事件结束 313 | 314 | app.listening_server_start() # HTTP API 服务器启动 315 | app.bot.start() # Bot启动 316 | ``` 317 | 318 | ------ 319 | 320 | ### 目前实现的CQ码 321 | 322 | | CQ码 | 功能 | 备注 | 323 | | ----------------------------------------- | -------- | ------------------------------------------------------------ | 324 | | [CQ:reply,id=abc123] | 回复消息 | 被动回复请务必带上此CQ码, 否则会被视为主动推送消息 | 325 | | [CQ:at,qq=123456] | 艾特用户 | 与官方<@!123456>对应 | 326 | | [CQ:image,file=...]
[CQ:image,url=...] | 发送图片 | 发送图片可以使用这两个CQ码
由于API限制, 发送图片仅支持一张, 并且需要自行配置图床(见上方代码示例)
接收到图片时, 目前仅会返回[CQ:image,url=...] | 327 | 328 | 329 | 330 | ------ 331 | 332 | 333 | 334 | ### 目前实现的API(基本同`onebot`): 335 | 336 | - 特别注意: 所有`用户ID`, `频道号`, `子频道号`, `消息ID`字段均为`string` 337 | 338 | | 接口 | 描述 | 备注 | 339 | | ------------------------------------------------------------ | ------------ | ------------------------------------------------------------ | 340 | | [/send_group_msg](https://github.com/botuniverse/onebot-11/blob/master/api/public.md#send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF) | 发送群消息 | `group_id`请填写`channel_id`
`auto_escape`为true时, 依然会解析[CQ:reply] | 341 | | [/get_group_member_info](https://github.com/botuniverse/onebot-11/blob/master/api/public.md#get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF) | 获取成员信息 | `group_id`请填写`guild_id`
仅`user_id`,`nickname`,`role`有效
额外增加头像URL: `avatar` | 342 | 343 | 344 | 345 | 346 | ------ 347 | 348 | 349 | 350 | ### 目前实现的API(与`onebot`不同) 351 | 352 | #### 获取自身信息 353 | 354 | - 接口: `/get_self_info` 355 | 356 | | 参数 | 类型 | 默认值 | 说明 | 357 | | ----- | ---- | ------ | ------------------ | 358 | | cache | bool | false | 是否使用缓存(可选) | 359 | 360 | - 返回参数 361 | 362 | | 参数 | 类型 | 说明 | 363 | | ------ | ------ | ------------ | 364 | | avatar | string | 头像url | 365 | | id | string | BotID | 366 | | username | string | Bot名 | 367 | 368 | 369 | 370 | #### 获取自身加入频道列表 371 | 372 | - 接口: `/get_self_guild_list` 373 | 374 | | 参数 | 类型 | 默认值 | 说明 | 375 | | ------ | ------ | ------ | -------------------------------------------------- | 376 | | cache | bool | false | 是否使用缓存(可选) | 377 | | before | string | - | 读此id之前的数据(可选, `before`/`after`只能二选一) | 378 | | after | string | - | 读此id之后的数据(可选, `before`/`after`只能二选一) | 379 | | limit | int | 100 | 每次拉取多少条数据(可选) | 380 | 381 | - 返回参数 382 | 383 | 见: https://bot.q.qq.com/wiki/develop/api/openapi/channel/get_channel.html 384 | 385 | 386 | 387 | #### 获取频道信息 388 | 389 | - 接口: `/get_guild_info` 390 | 391 | | 参数 | 类型 | 默认值 | 说明 | 392 | | -------- | ------ | ------ | ------ | 393 | | guild_id | string | - | 频道id | 394 | 395 | - 返回参数 396 | 397 | 见: https://bot.q.qq.com/wiki/develop/api/openapi/guild/get_guild.html 398 | 399 | 400 | 401 | #### 获取子频道信息 402 | 403 | - 接口: `/get_channel_info` 404 | 405 | | 参数 | 类型 | 默认值 | 说明 | 406 | | ---------- | ------ | ------ | -------- | 407 | | channel_id | string | - | 子频道id | 408 | 409 | - 返回参数 410 | 411 | 见: https://bot.q.qq.com/wiki/develop/api/openapi/channel/get_channel.html 412 | 413 | 414 | 415 | #### 获取子频道列表 416 | 417 | - 接口: `/get_channel_list` 418 | 419 | | 参数 | 类型 | 默认值 | 说明 | 420 | | -------- | ------ | ------ | ------ | 421 | | guild_id | string | - | 频道id | 422 | 423 | - 返回参数 424 | 425 | 见: https://bot.q.qq.com/wiki/develop/api/openapi/channel/get_channels.html 426 | 427 | 428 | 429 | #### 获取指定消息 430 | 431 | - 接口: `/get_message` 432 | 433 | | 参数 | 类型 | 默认值 | 说明 | 434 | | ---------- | ------ | ------ | ------ | 435 | | message_id | string | - | 消息id | 436 | | channel_id | string | - | 频道id | 437 | 438 | - 返回参数 439 | 440 | 见: https://bot.q.qq.com/wiki/develop/api/openapi/message/get_message_of_id.html 441 | 442 | 443 | 444 | ------ 445 | 446 | ### 目前实现的Event(基本同`onebot`) 447 | 448 | - 特别注意: 所有`用户ID`, `频道号`, `子频道号`, `消息ID`字段均为`string` 449 | 450 | #### 收到艾特消息 451 | 452 | - 见: https://github.com/botuniverse/onebot-11/blob/master/event/message.md#%E7%BE%A4%E6%B6%88%E6%81%AF 453 | 454 | - 注意: 455 | 456 | - 注意上面的`特别注意` 457 | - `anonymous`字段恒为`null` 458 | - `sender`字段中仅`user_id`, `nickname`, `role`有效 459 | 460 | 461 | 462 | 463 | #### 成员增加事件 464 | 465 | - 见: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%A2%9E%E5%8A%A0 466 | - 注意: 467 | - 注意上面的`特别注意` 468 | 469 | - `sub_type`恒为`approve` 470 | 471 | 472 | 473 | 474 | #### 成员减少事件 475 | 476 | - 见: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md#%E7%BE%A4%E6%88%90%E5%91%98%E5%87%8F%E5%B0%91 477 | - 注意: 478 | - 注意上面的`特别注意` 479 | - `sub_type`恒为`leave` 480 | 481 | ------ 482 | 483 | 484 | 485 | ### 目前实现的Event(与`onebot`差别较大) 486 | 487 | - 通用字段 488 | 489 | | 参数 | 类型 | 默认值 | 说明 | 490 | | ----------- | ------ | -------- | ------------------------------ | 491 | | time | int | - | 消息接收时间戳 | 492 | | self_id | string | - | 自身id | 493 | | post_type | string | `notice` | 上报类型 | 494 | | notice_type | string | - | 通知类型 | 495 | | sub_type | string | - | 通知子类型 | 496 | | user_id | string | 空字符串 | 触发者ID, 仅在对应事件有值 | 497 | | guild_id | string | 空字符串 | 触发频道ID, 仅在对应事件有值 | 498 | | channel_id | string | 空字符串 | 触发子频道ID, 仅在对应事件有值 | 499 | | data | - | - | 每个事件均不同, 见下方文档 | 500 | 501 | 502 | 503 | #### Bot加入频道事件 504 | 505 | | 参数 | 类型 | 值 | 506 | | ----------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 507 | | post_type | string | `notice` | 508 | | notice_type | string | `guild_create` | 509 | | sub_type | string | `guild_create` | 510 | | guild_id | string | 频道ID | 511 | | data | [Guild](https://bot.q.qq.com/wiki/develop/api/openapi/guild/model.html#guild) | 见: [腾讯机器人文档](https://bot.q.qq.com/wiki/develop/api/gateway/guild.html#guild-create) | 512 | 513 | 514 | #### 频道信息更新事件 515 | 516 | | 参数 | 类型 | 值 | 517 | | --------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 518 | | post_type | string | `notice` | 519 | | sub_type | string | `guild_update` | 520 | | guild_id | string | 频道ID | 521 | | data | [Guild](https://bot.q.qq.com/wiki/develop/api/openapi/guild/model.html#guild) | 见: [腾讯机器人文档](https://bot.q.qq.com/wiki/develop/api/gateway/guild.html#guild-update) | 522 | 523 | #### 机器人离开频道/频道被解散事件 524 | 525 | | 参数 | 类型 | 值 | 526 | | ----------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 527 | | post_type | string | `notice` | 528 | | notice_type | string | `guild_update` | 529 | | sub_type | string | `guild_update` | 530 | | guild_id | string | 频道ID | 531 | | data | [Guild](https://bot.q.qq.com/wiki/develop/api/openapi/guild/model.html#guild) | 见: [腾讯机器人文档](https://bot.q.qq.com/wiki/develop/api/gateway/guild.html#guild-delete) | 532 | 533 | #### 子频道创建事件 534 | 535 | | 参数 | 类型 | 值 | 536 | | ----------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 537 | | post_type | string | `notice` | 538 | | notice_type | string | `channel_create` | 539 | | sub_type | string | `channel_create` | 540 | | guild_id | string | 频道ID | 541 | | channel_id | string | 子频道ID | 542 | | data | [Channel](https://bot.q.qq.com/wiki/develop/api/openapi/channel/model.html#channel) | 见: [腾讯机器人文档](https://bot.q.qq.com/wiki/develop/api/gateway/channel.html#channel-create) | 543 | 544 | #### 子频道信息更新事件 545 | 546 | | 参数 | 类型 | 值 | 547 | | ----------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 548 | | post_type | string | `notice` | 549 | | notice_type | string | `channel_update` | 550 | | sub_type | string | `channel_update` | 551 | | guild_id | string | 频道ID | 552 | | channel_id | string | 子频道ID | 553 | | data | [Channel](https://bot.q.qq.com/wiki/develop/api/openapi/channel/model.html#channel) | 见: [腾讯机器人文档](https://bot.q.qq.com/wiki/develop/api/gateway/channel.html#channel-update) | 554 | 555 | #### 子频道被删除事件 556 | 557 | | 参数 | 类型 | 值 | 558 | | ----------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 559 | | post_type | string | `notice` | 560 | | notice_type | string | `channel_delete` | 561 | | sub_type | string | `channel_delete` | 562 | | guild_id | string | 频道ID | 563 | | channel_id | string | 子频道ID | 564 | | data | [Channel](https://bot.q.qq.com/wiki/develop/api/openapi/channel/model.html#channel) | 见: [腾讯机器人文档](https://bot.q.qq.com/wiki/develop/api/gateway/channel.html#channel-delete) | 565 | 566 | -------------------------------------------------------------------------------- /bot_api/__init__.py: -------------------------------------------------------------------------------- 1 | from .sdk_main import * 2 | from . import api 3 | from . import models 4 | from . import structs 5 | from . import utils 6 | from .models import BotCallingAPIError 7 | -------------------------------------------------------------------------------- /bot_api/api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from . import structs 4 | import typing as t 5 | from . import models 6 | from .logger import BotLogger 7 | 8 | 9 | class BotApi(BotLogger): 10 | def __init__(self, appid: int, token: str, secret: str, debug: bool, sandbox: bool, api_return_pydantic=False, 11 | output_log=True, log_path="", raise_api_error=False): 12 | super().__init__(debug=debug, write_out_log=output_log, log_path=log_path) 13 | self.appid = appid 14 | self.token = token 15 | self.secret = secret 16 | self.base_api = "https://sandbox.api.sgroup.qq.com" if sandbox else "https://api.sgroup.qq.com" 17 | self.debug = debug 18 | self.api_return_pydantic = api_return_pydantic 19 | self.raise_api_error = raise_api_error 20 | 21 | self.__headers = { 22 | 'Authorization': f'Bot {self.appid}.{self.token}', 23 | 'Content-Type': 'application/json' 24 | } 25 | 26 | self._cache = {} 27 | 28 | def _throwerr(self, error_response: str, error_message="", trace_id=None): 29 | """ 30 | 抛出API异常 31 | :param error_response: API返回信息 32 | :param error_message: 自定义描述 33 | :param trace_id: X-TPS-TRACE-ID 34 | """ 35 | if self.raise_api_error: 36 | raise models.BotCallingAPIError(error_response, error_message, x_tps_trace_id=trace_id) 37 | 38 | def _tlogger(self, msg: str, debug=False, warning=False, error=False, error_resp=None, traceid=None): 39 | smsg = f"{msg}\nX-Tps-trace-ID: {traceid}\n" if traceid is not None else msg 40 | super().logger(msg=smsg, debug=debug, warning=warning, error=error) 41 | if error_resp is not None and error: 42 | self._throwerr(error_response=error_resp, error_message=smsg, trace_id=traceid) 43 | 44 | def _retter(self, response: requests.Response, wrong_text: str, data_model, retstr: bool, data_type=0): 45 | """ 46 | 返回器 47 | :param response: api返回值 48 | :param wrong_text: 错误信息 49 | :param data_model: pydantic_model信息 50 | :param retstr: 是否强制返回纯文本 51 | :param data_type: pydantic_model类型: 0-str, 1-List 52 | :return: 53 | """ 54 | get_response = response.text 55 | trace_id = response.headers.get("X-Tps-trace-ID") 56 | data = json.loads(get_response) 57 | if "code" in data: 58 | self._tlogger(f"{wrong_text}: {get_response}", error=True, error_resp=get_response, traceid=trace_id) 59 | # if retstr: 60 | return get_response 61 | # return None 62 | elif self.api_return_pydantic and not retstr: 63 | try: 64 | if data_type == 0: 65 | return data_model(**data) 66 | elif data_type == 1: 67 | return [data_model(**g) for g in data] 68 | else: 69 | self._tlogger("retter_data_type错误, 将原样返回", warning=True) 70 | 71 | return get_response 72 | except Exception as sb: 73 | self._tlogger("请求转换为 Basemodel 失败, 将原样返回", error=True) 74 | self._tlogger(f"请求原文: {get_response}", debug=True, traceid=trace_id) 75 | print(sb) 76 | return get_response 77 | else: 78 | return get_response 79 | 80 | def api_create_dms(self, recipient_id, source_guild_id, retstr=False) -> t.Union[structs.DMS, str]: 81 | """ 82 | 创建私信会话 83 | :param recipient_id: 接收者 id 84 | :param source_guild_id: 源频道 id 85 | :param retstr: 强制返回文本 86 | :return: DMS对象 87 | """ 88 | url = f"{self.base_api}/users/@me/dms" 89 | payload = { 90 | "recipient_id": recipient_id, 91 | "source_guild_id": source_guild_id 92 | } 93 | response = requests.request("POST", url, data=json.dumps(payload), headers=self.__headers) 94 | return self._retter(response, "创建私信会话失败", structs.DMS, retstr, data_type=0) 95 | 96 | def api_reply_message(self, event: structs.Message, content="", image_url="", retstr=False, 97 | embed=None, ark=None, others_parameter: t.Optional[t.Dict] = None, at_user=True, 98 | message_reference=True, **kwargs) \ 99 | -> t.Union[str, structs.Message, None]: 100 | """ 101 | 快速回复消息, 支持频道消息/私聊消息 102 | :param event: 原event 103 | :param content: 消息内容, 可空 104 | :param image_url: 图片url, 可空 105 | :param retstr: 调用此API后返回纯文本 106 | :param embed: embed消息, 可空 107 | :param ark: ark消息, 可空 108 | :param others_parameter: 其它自定义字段 109 | :param at_user: 艾特被回复者 110 | :param message_reference: 引用对应消息 111 | """ 112 | event_type = event.message_type_sdk 113 | if at_user: 114 | if "<@" not in content and event_type != "private": 115 | content = f"<@{event.author.id}>\n{content}" 116 | 117 | ref_type = 1 if message_reference else 0 118 | 119 | if event_type == "guild": 120 | return self.api_send_message(channel_id=event.channel_id, msg_id=event.id, content=content, 121 | image_url=image_url, retstr=retstr, embed=embed, ark=ark, 122 | others_parameter=others_parameter, message_reference_type=ref_type, **kwargs) 123 | elif event_type == "private": 124 | return self.api_send_private_message(guild_id=event.guild_id, channel_id=event.channel_id, msg_id=event.id, 125 | content=content, image_url=image_url, 126 | retstr=retstr, embed=embed, ark=ark, others_parameter=others_parameter, 127 | message_reference_type=ref_type, **kwargs) 128 | else: 129 | self.logger("reply_message() - 无法识别传入的event", error=True) 130 | return None 131 | 132 | def api_send_message(self, channel_id, msg_id="", content="", image_url="", retstr=False, 133 | embed=None, ark=None, message_reference_type=0, message_reference_id=None, 134 | others_parameter: t.Optional[t.Dict] = None, **kwargs) \ 135 | -> t.Union[str, structs.Message, None]: 136 | """ 137 | 发送消息 138 | :param channel_id: 子频道ID 139 | :param msg_id: 消息ID, 可空 140 | :param content: 消息内容, 可空 141 | :param image_url: 图片url, 可空 142 | :param retstr: 调用此API后返回纯文本 143 | :param embed: embed消息, 可空 144 | :param ark: ark消息, 可空 145 | :param message_reference_type: 引用消息, 0-不引用; 1-引用消息, 不忽略获取引用消息详情错误; 2- 引用消息, 忽略获取引用消息详情错误 146 | :param message_reference_id: 引用消息的ID, 若为空, 则使用"msg_id"字段的值 147 | :param others_parameter: 其它自定义字段 148 | """ 149 | return self._api_send_message(channel_id=channel_id, msg_id=msg_id, content=content, image_url=image_url, 150 | retstr=retstr, embed=embed, ark=ark, 151 | message_reference_type=message_reference_type, 152 | message_reference_id=message_reference_id, others_parameter=others_parameter, 153 | **kwargs) 154 | 155 | def api_send_private_message(self, guild_id, channel_id, msg_id="", content="", image_url="", retstr=False, 156 | embed=None, ark=None, message_reference_type=0, message_reference_id=None, 157 | others_parameter: t.Optional[t.Dict] = None, **kwargs): 158 | """ 159 | 发送私聊消息 160 | :param guild_id: 频道ID 161 | :param channel_id: 子频道ID 162 | :param msg_id: 消息ID, 可空 163 | :param content: 消息内容, 可空 164 | :param image_url: 图片url, 可空 165 | :param retstr: 调用此API后返回纯文本 166 | :param embed: embed消息, 可空 167 | :param ark: ark消息, 可空 168 | :param message_reference_type: 引用消息, 0-不引用; 1-引用消息, 不忽略获取引用消息详情错误; 2- 引用消息, 忽略获取引用消息详情错误 169 | :param message_reference_id: 引用消息的ID, 若为空, 则使用"msg_id"字段的值 170 | :param others_parameter: 其它自定义字段 171 | """ 172 | return self._api_send_message(channel_id=channel_id, msg_id=msg_id, content=content, image_url=image_url, 173 | retstr=retstr, embed=embed, ark=ark, others_parameter=others_parameter, 174 | message_reference_type=message_reference_type, 175 | message_reference_id=message_reference_id, 176 | guild_id=guild_id, **kwargs) 177 | 178 | def _api_send_message(self, channel_id, msg_id="", content="", image_url="", retstr=False, 179 | embed=None, ark=None, others_parameter: t.Optional[t.Dict] = None, guild_id=None, 180 | message_reference_type=0, message_reference_id=None, is_markdown=False) \ 181 | -> t.Union[str, structs.Message, None]: 182 | """ 183 | 发送消息 184 | :param channel_id: 子频道ID 185 | :param msg_id: 消息ID, 可空 186 | :param content: 消息内容, 可空 187 | :param image_url: 图片url, 可空 188 | :param retstr: 调用此API后返回纯文本 189 | :param embed: embed消息, 可空 190 | :param ark: ark消息, 可空 191 | :param others_parameter: 其它自定义字段 192 | :param guild_id: 填写该字段后将发送私聊消息 193 | :param message_reference_type: 引用消息, 0-不引用; 1-引用消息, 不忽略获取引用消息详情错误; 2- 引用消息, 忽略获取引用消息详情错误 194 | :param message_reference_id: 引用消息的ID, 若为空, 则使用"msg_id"字段的值 195 | """ 196 | 197 | url = f"{self.base_api}/channels/{channel_id}/messages" if guild_id is None else \ 198 | f"{self.base_api}/dms/{guild_id}/messages" 199 | if content == "" and image_url == "" and embed is None and ark is None and others_parameter is None: 200 | self._tlogger("消息为空, 请检查", error=True) 201 | return None 202 | 203 | if content != "": 204 | if is_markdown: 205 | _c = {"markdown": {"content": content}} 206 | else: 207 | _c = {"content": content} 208 | else: 209 | _c = None 210 | 211 | _im = {"image": image_url} if image_url != "" else None 212 | _msgid = {"msg_id": msg_id} if msg_id != "" else None 213 | _embed = embed.ark_to_json() if callable(getattr(embed, "ark_to_json", None)) else None 214 | _ark = ark.ark_to_json() if callable(getattr(ark, "ark_to_json", None)) else None 215 | 216 | message_reference_id = msg_id if message_reference_id is None else message_reference_id 217 | _ref = {"message_reference": {"message_id": message_reference_id, 218 | "ignore_get_message_error": False if message_reference_type == 1 else True}} \ 219 | if message_reference_id and message_reference_type else None 220 | 221 | def merge_dict(*args) -> dict: 222 | merged = {} 223 | for _d in args: 224 | if _d is not None: 225 | merged = {**merged, **_d} 226 | return merged 227 | 228 | payload = json.dumps(merge_dict(_c, _im, _msgid, _embed, _ark, _ref, others_parameter)) 229 | 230 | response = requests.request("POST", url, headers=self.__headers, data=payload) 231 | return self._retter(response, "发送信息失败", structs.Message, retstr) 232 | 233 | def api_mute_guild(self, guild_id, mute_seconds="", mute_end_timestamp="", 234 | user_ids: t.Optional[t.List[str]] = None) -> t.Union[str, dict, t.List[str]]: 235 | """ 236 | 全频道禁言, 秒数/时间戳二选一 237 | :param guild_id: 频道ID 238 | :param mute_seconds: 禁言秒数 239 | :param mute_end_timestamp: 禁言截止时间戳 240 | :param user_ids: 批量禁言的用户ID列表, 若此项为None, 则全员禁言 241 | :return: 若成功, 返回空字符串(全员禁言) 或 被禁言的用户id列表(批量禁言); 失败则返回错误信息 242 | """ 243 | url = f"{self.base_api}/guilds/{guild_id}/mute" 244 | _body = {"mute_end_timestamp": f"{mute_end_timestamp}"} if mute_end_timestamp != "" else \ 245 | {"mute_seconds": f"{mute_seconds}"} 246 | if user_ids is None: 247 | response = requests.request("PATCH", url, data=json.dumps(_body), headers=self.__headers) 248 | if response.status_code != 204: 249 | data = response.text 250 | self._tlogger(f"禁言频道失败: {data}", error=True, error_resp=data, 251 | traceid=response.headers.get("X-Tps-trace-ID")) 252 | return data 253 | else: 254 | return "" 255 | else: 256 | _body["user_ids"] = user_ids 257 | response = requests.request("PATCH", url, data=json.dumps(_body), headers=self.__headers) 258 | if response.status_code != 200: 259 | self._tlogger(f"批量禁言失败: {response.text}", error=True, error_resp=response.text, 260 | traceid=response.headers.get("X-Tps-trace-ID")) 261 | return response.text 262 | else: 263 | udata = json.loads(response.text) 264 | return udata["user_ids"] if f"{user_ids}" in udata else udata 265 | 266 | def api_mute_member(self, guild_id, member_id, mute_seconds="", mute_end_timestamp="") -> str: 267 | """ 268 | 指定用户禁言, 秒数/时间戳二选一 269 | :param guild_id: 频道ID 270 | :param member_id: 用户ID 271 | :param mute_seconds: 禁言秒数 272 | :param mute_end_timestamp: 禁言截止时间戳 273 | :return: 若成功, 返回空字符串; 失败则返回错误信息 274 | """ 275 | url = f"{self.base_api}/guilds/{guild_id}/members/{member_id}/mute" 276 | _body = {"mute_end_timestamp": f"{mute_end_timestamp}"} if mute_end_timestamp != "" else \ 277 | {"mute_seconds": f"{mute_seconds}"} 278 | response = requests.request("PATCH", url, data=json.dumps(_body), headers=self.__headers) 279 | if response.status_code != 204: 280 | data = response.text 281 | self._tlogger(f"禁言成员失败: {data}", error=True, error_resp=data, 282 | traceid=response.headers.get("X-Tps-trace-ID")) 283 | return data 284 | else: 285 | return "" 286 | 287 | def api_guild_roles_list_get(self, guild_id, retstr=False) -> t.Union[str, structs.RetModel.GetGuildRole]: 288 | """ 289 | 获取频道身份组列表 290 | :param guild_id: 频道ID 291 | :param retstr: 强制返回纯文本 292 | :return: 频道身份组信息 293 | """ 294 | url = f"{self.base_api}/guilds/{guild_id}/roles" 295 | response = requests.request("GET", url, headers=self.__headers) 296 | return self._retter(response, "获取频道身份组列表失败", structs.RetModel.GetGuildRole, retstr, data_type=0) 297 | 298 | def api_guild_role_create(self, guild_id, name="", color=-1, hoist=1, retstr=False) \ 299 | -> t.Union[str, structs.RetModel.CreateGuildRole]: 300 | """ 301 | 创建频道身份组 302 | :param guild_id: 频道ID 303 | :param name: 名称 304 | :param color: ARGB的HEX十六进制颜色值转换后的十进制数值 305 | :param hoist: 在成员列表中单独展示: 0-否, 1-是 306 | :param retstr: 强制返回纯文本 307 | """ 308 | url = f"{self.base_api}/guilds/{guild_id}/roles" 309 | body = models.role_body(name, color, hoist) 310 | response = requests.request("POST", url, data=json.dumps(body), headers=self.__headers) 311 | return self._retter(response, "创建频道身份组失败", structs.RetModel.CreateGuildRole, retstr, data_type=0) 312 | 313 | def api_guild_role_change(self, guild_id, role_id, name="", color=-1, hoist=1, retstr=False) \ 314 | -> t.Union[str, structs.RetModel.ChangeGuildRole]: 315 | """ 316 | 修改频道身份组 317 | :param guild_id: 频道ID 318 | :param role_id: 身份组ID 319 | :param name: 名称 320 | :param color: ARGB的HEX十六进制颜色值转换后的十进制数值 321 | :param hoist: 在成员列表中单独展示: 0-否, 1-是 322 | :param retstr: 强制返回纯文本 323 | """ 324 | url = f"{self.base_api}/guilds/{guild_id}/roles/{role_id}" 325 | body = models.role_body(name, color, hoist) 326 | response = requests.request("PATCH", url, data=json.dumps(body), headers=self.__headers) 327 | return self._retter(response, "修改频道身份组失败", structs.RetModel.ChangeGuildRole, retstr, data_type=0) 328 | 329 | def api_guild_role_remove(self, guild_id, role_id): 330 | """ 331 | 删除频道身份组 332 | :param guild_id: 频道ID 333 | :param role_id: 身份组ID 334 | :return: 成功返回空字符串, 失败返回报错 335 | """ 336 | url = f"{self.base_api}/guilds/{guild_id}/roles/{role_id}" 337 | response = requests.request("DELETE", url, headers=self.__headers) 338 | if response.status_code != 204: 339 | self._tlogger(f"删除频道身份组失败: {response.text}", error=True, error_resp=response.text, 340 | traceid=response.headers.get("X-Tps-trace-ID")) 341 | return response.text 342 | else: 343 | return "" 344 | 345 | def api_guild_role_member_add(self, guild_id, role_id, user_id, channel_id=""): 346 | """ 347 | 增加频道身份组成员 348 | :param guild_id: 频道ID 349 | :param role_id: 身份组ID 350 | :param user_id: 用户ID 351 | :param channel_id: 如果要增加的身份组ID是 5-子频道管理员, 则需要填写channel_id指定频道 352 | :return: 成功返回空字符串, 失败返回报错 353 | """ 354 | return self._request_guild_role_member(guild_id, role_id, user_id, channel_id, "PUT") 355 | 356 | def api_guild_role_member_remove(self, guild_id, role_id, user_id, channel_id=""): 357 | """ 358 | 移除频道身份组成员 359 | :param guild_id: 频道ID 360 | :param role_id: 身份组ID 361 | :param user_id: 用户ID 362 | :param channel_id: 如果要增加的身份组ID是 5-子频道管理员, 则需要填写channel_id指定频道 363 | :return: 成功返回空字符串, 失败返回报错 364 | """ 365 | return self._request_guild_role_member(guild_id, role_id, user_id, channel_id, "DELETE") 366 | 367 | def api_announces_create(self, guild_id, message_id=None, channel_id=None, announces_type=None, 368 | recommend_channels=None, retstr=False) \ 369 | -> t.Union[str, structs.Announces]: 370 | """ 371 | 创建频道公告 372 | :param guild_id: 频道ID 373 | :param message_id: 选填,消息 id,message_id 有值则优选将某条消息设置为成员公告 374 | :param channel_id: 选填,子频道 id,message_id 有值则为必填。 375 | :param announces_type: 选填,公告类别 0:成员公告,1:欢迎公告,默认为成员公告 376 | :param recommend_channels: 选填,推荐子频道列表,会一次全部替换推荐子频道列表, 填写格式: [[子频道id, 推荐语], [子频道id, 推荐语], ...] 377 | :param retstr: 强制返回纯文本 378 | :return: Announces 379 | """ 380 | url = f"{self.base_api}/guilds/{guild_id}/announces" 381 | body = {} 382 | if message_id is not None: 383 | body["message_id"] = message_id 384 | if channel_id is not None: 385 | body["channel_id"] = channel_id 386 | if announces_type is not None: 387 | body["announces_type"] = announces_type 388 | if recommend_channels is not None: 389 | tl = [] 390 | for channel_id, introduce in recommend_channels: 391 | tl.append({"channel_id": channel_id, "introduce": introduce}) 392 | body[recommend_channels] = tl 393 | 394 | response = requests.request("POST", url, data=json.dumps(body), headers=self.__headers) 395 | return self._retter(response, "创建频道公告失败", structs.Announces, retstr, data_type=0) 396 | 397 | def api_announces_global_remove(self, guild_id, message_id="all"): 398 | """ 399 | 删除频道公告 400 | :param guild_id: 频道ID 401 | :param message_id: 消息ID. message_id 有值时,会校验 message_id 合法性,若不校验 message_id,请将 message_id 设置为 all 402 | :return: 成功返回空字符串, 失败返回错误信息 403 | """ 404 | url = f"{self.base_api}/guilds/{guild_id}/announces/{message_id}" 405 | response = requests.request("DELETE", url, headers=self.__headers) 406 | if response.status_code != 204: 407 | self._tlogger(f"删除频道公告失败: {response.text}", error_resp=response.text, 408 | traceid=response.headers.get("X-Tps-trace-ID")) 409 | return response.text 410 | else: 411 | return "" 412 | 413 | def api_permissions_get_channel(self, channel_id, user_id, retstr=False) \ 414 | -> t.Union[str, structs.ChannelPermissions]: 415 | """ 416 | 获取指定子频道的权限 417 | :param channel_id: 子频道ID 418 | :param user_id: 用户ID 419 | :param retstr: 强制返回纯文本 420 | :return: ChannelPermissions, role_id必为None 421 | """ 422 | url = f"{self.base_api}/channels/{channel_id}/members/{user_id}/permissions" 423 | response = requests.request("GET", url, headers=self.__headers) 424 | return self._retter(response, "获取指定子频道的权限失败", structs.ChannelPermissions, retstr, data_type=0) 425 | 426 | def api_permissions_change_channel(self, channel_id, user_id, add: str, remove: str, **kwargs): 427 | """ 428 | 修改指定子频道的权限, 详见: https://bot.q.qq.com/wiki/develop/api/openapi/channel_permissions/put_channel_permissions.html 429 | :param channel_id: 子频道ID 430 | :param user_id: 用户ID 431 | :param add: 字符串形式的位图表示赋予用户的权限 432 | :param remove: 字符串形式的位图表示赋予用户的权限 433 | :return: 成功返回空字符串, 失败返回错误信息 434 | """ 435 | if "setrole" in kwargs: 436 | url = f"{self.base_api}/channels/{channel_id}/members/{user_id}/permissions" 437 | ft = "修改指定子频道身份组的权限失败" 438 | else: 439 | url = f"{self.base_api}/channels/{channel_id}/roles/{user_id}/permissions" 440 | ft = "修改指定子频道的权限失败" 441 | body = {"add": add, "remove": remove} 442 | response = requests.request("PUT", url, data=json.dumps(body), headers=self.__headers) 443 | if response.status_code != 204: 444 | self._tlogger(f"{ft}: {response.text}", error_resp=response.text, 445 | traceid=response.headers.get("X-Tps-trace-ID")) 446 | return response.text 447 | else: 448 | return "" 449 | 450 | def api_permissions_get_channel_group(self, channel_id, role_id, retstr=False) \ 451 | -> t.Union[str, structs.ChannelPermissions]: 452 | """ 453 | 获取指定子频道身份组的权限 454 | :param channel_id: 子频道ID 455 | :param role_id: 身份组ID 456 | :param retstr: 强制返回纯文本 457 | :return: ChannelPermissions, user_id必为None 458 | """ 459 | url = f"{self.base_api}/channels/{channel_id}/roles/{role_id}/permissions" 460 | response = requests.request("GET", url, headers=self.__headers) 461 | return self._retter(response, "获取指定子频道身份组的权限失败", structs.ChannelPermissions, retstr, data_type=0) 462 | 463 | def api_permissions_change_channel_group(self, channel_id, role_id, add: str, remove: str): 464 | """ 465 | 修改指定子频道身份组的权限, 详见: https://bot.q.qq.com/wiki/develop/api/openapi/channel_permissions/put_channel_permissions.html 466 | :param channel_id: 子频道ID 467 | :param role_id: 用户ID 468 | :param add: 字符串形式的位图表示赋予用户的权限 469 | :param remove: 字符串形式的位图表示赋予用户的权限 470 | :return: 成功返回空字符串, 失败返回错误信息 471 | """ 472 | return self.api_permissions_change_channel(channel_id, role_id, add, remove, setrole=1) 473 | 474 | def api_audio_control(self, channel_id, audio_url: str, status: int, text=""): 475 | """ 476 | 音频控制 477 | :param channel_id: 子频道ID 478 | :param audio_url: 音频url 479 | :param status: 播放状态, 见: structs.AudioControlSTATUS 480 | :param text: 状态文本(比如: 简单爱-周杰伦),可选,status为0时传,其他操作不传 481 | :return: 成功返回"{}" 482 | """ 483 | url = f"{self.base_api}/channels/{channel_id}/audio" 484 | body = models.audio_control(audio_url, status, text) 485 | response = requests.request("POST", url, data=json.dumps(body), headers=self.__headers) 486 | if response.text != "{}": 487 | self._tlogger(f"音频控制失败: {response.text}", error_resp=response.text, 488 | traceid=response.headers.get("X-Tps-trace-ID")) 489 | return response.text 490 | 491 | def api_get_self_guilds(self, before="", after="", limit="100", use_cache=False, retstr=False) \ 492 | -> t.Union[str, t.List[structs.Guild], None]: 493 | """ 494 | 获取Bot加入的频道列表 495 | :param before: 读此id之前的数据(before/after 只能二选一) 496 | :param after: 读此id之后的数据(before/after 只能二选一) 497 | :param limit: 每次拉取条数 498 | :param use_cache: 使用缓存 499 | :param retstr: 强制返回纯文本 500 | """ 501 | return self.get_self_guilds(before=before, after=after, limit=limit, use_cache=use_cache, retstr=retstr) 502 | 503 | def api_get_self_info(self, use_cache=False): 504 | """ 505 | 获取Bot自身信息 506 | """ 507 | return self.get_self_info(use_cache=use_cache) 508 | 509 | def api_get_message(self, channel_id, message_id, retstr=False) -> t.Union[str, structs.Message, None]: 510 | """ 511 | 获取指定消息 512 | :param channel_id: 子频道id 513 | :param message_id: 消息id 514 | :param retstr: 强制返回纯文本 515 | """ 516 | return self.get_message(channel_id=channel_id, message_id=message_id, retstr=retstr) 517 | 518 | def api_get_guild_channel_list(self, guild_id, retstr=False) -> t.Union[str, t.List[structs.Channel], None]: 519 | """ 520 | 获取频道内子频道列表 521 | :param guild_id: 频道id 522 | :param retstr: 强制返回纯文本 523 | """ 524 | return self.get_guild_channel_list(guild_id=guild_id, retstr=retstr) 525 | 526 | def api_get_channel_info(self, channel_id, retstr=False) -> t.Union[str, structs.Channel, None]: 527 | """ 528 | 获取子频道信息 529 | :param channel_id: 频道id 530 | :param retstr: 强制返回纯文本 531 | """ 532 | return self.get_channel_info(channel_id=channel_id, retstr=retstr) 533 | 534 | def api_get_guild_user_info(self, guild_id, member_id, retstr=False) -> t.Union[str, structs.Member, None]: 535 | """ 536 | 获取频道用户信息 537 | :param guild_id: 频道id 538 | :param member_id: 用户id 539 | :param retstr: 强制返回纯文本 540 | """ 541 | return self.get_guild_user_info(guild_id=guild_id, member_id=member_id, retstr=retstr) 542 | 543 | def api_get_guild_info(self, guild_id, retstr=False) -> t.Union[str, structs.Guild, None]: 544 | """ 545 | 获取频道信息 546 | :param guild_id: 频道id 547 | :param retstr: 强制返回纯文本 548 | """ 549 | return self.get_guild_info(guild_id=guild_id, retstr=retstr) 550 | 551 | def api_get_schedule_list(self, channel_id, retstr=False) -> t.Union[str, t.List[structs.Schedule], None]: 552 | """ 553 | 获取子频道日程列表 554 | :param channel_id: 子频道ID 555 | :param retstr: 强制返回纯文本 556 | :return: 日程列表(若为空, 则返回 None) 557 | """ 558 | url = f"{self.base_api}/channels/{channel_id}/schedules" 559 | response = requests.request("GET", url, headers=self.__headers) 560 | return self._retter(response, "获取日程列表失败", structs.Schedule, retstr, data_type=1) 561 | 562 | def api_get_schedule(self, channel_id, schedule_id, retstr=False) -> t.Union[str, structs.Schedule, None]: 563 | """ 564 | 获取单个日程信息 565 | :param channel_id: 子频道ID 566 | :param schedule_id: 日程ID 567 | :param retstr: 强制返回纯文本 568 | :return: 单个日程信息(若为空, 则返回 None) 569 | """ 570 | url = f"{self.base_api}/channels/{channel_id}/schedules/{schedule_id}" 571 | response = requests.request("GET", url, headers=self.__headers) 572 | return self._retter(response, "获取日程信息失败", structs.Schedule, retstr, data_type=0) 573 | 574 | def api_schedule_create(self, channel_id, name: str, description: str, start_timestamp: str, end_timestamp: str, 575 | jump_channel_id: str, remind_type: str, retstr=False) -> t.Union[str, structs.Schedule, 576 | None]: 577 | """ 578 | 创建日程 579 | :param channel_id: 子频道ID 580 | :param name: 日程标题 581 | :param description: 日程描述 582 | :param start_timestamp: 开始时间, 13位时间戳 583 | :param end_timestamp: 结束时间, 13位时间戳 584 | :param jump_channel_id: 日程跳转频道ID 585 | :param remind_type: 日程提醒类型, 见: https://bot.q.qq.com/wiki/develop/api/openapi/schedule/model.html#remindtype 586 | :param retstr: 强制返回纯文本 587 | :return: 新创建的日程 588 | """ 589 | url = f"{self.base_api}/channels/{channel_id}/schedules" 590 | payload = json.dumps(models.schedule_json(name, description, start_timestamp, end_timestamp, 591 | jump_channel_id, remind_type)) 592 | response = requests.request("POST", url, headers=self.__headers, data=payload) 593 | return self._retter(response, "创建日程失败", structs.Schedule, retstr, data_type=0) 594 | 595 | def api_schedule_change(self, channel_id, schedule_id, name: str, description: str, start_timestamp: str, 596 | end_timestamp: str, jump_channel_id: str, remind_type: str, retstr=False) \ 597 | -> t.Union[str, structs.Schedule, None]: 598 | """ 599 | 修改日程 600 | :param channel_id: 子频道ID 601 | :param schedule_id: 日程ID 602 | :param name: 日程标题 603 | :param description: 日程描述 604 | :param start_timestamp: 开始时间, 13位时间戳 605 | :param end_timestamp: 结束时间, 13位时间戳 606 | :param jump_channel_id: 日程跳转频道ID 607 | :param remind_type: 日程提醒类型, 见: https://bot.q.qq.com/wiki/develop/api/openapi/schedule/model.html#remindtype 608 | :param retstr: 强制返回纯文本 609 | :return: 修改后的日程 610 | """ 611 | url = f"{self.base_api}/channels/{channel_id}/schedules/{schedule_id}" 612 | payload = json.dumps(models.schedule_json(name, description, start_timestamp, end_timestamp, 613 | jump_channel_id, remind_type)) 614 | response = requests.request("PATCH", url, headers=self.__headers, data=payload) 615 | return self._retter(response, "修改日程失败", structs.Schedule, retstr, data_type=0) 616 | 617 | def api_schedule_delete(self, channel_id, schedule_id): 618 | """ 619 | 删除日程 620 | :param channel_id: 子频道ID 621 | :param schedule_id: 日程ID 622 | :return: 成功返回空字符串, 失败返回错误信息 623 | """ 624 | url = f"{self.base_api}/channels/{channel_id}/schedules/{schedule_id}" 625 | response = requests.request("DELETE", url, headers=self.__headers) 626 | if response.status_code != 204: 627 | data = response.text 628 | self._tlogger(f"日程删除失败: {data}", error_resp=response.text, 629 | traceid=response.headers.get("X-Tps-trace-ID")) 630 | return data 631 | else: 632 | return "" 633 | 634 | def api_message_recall(self, channel_id, message_id, hidetip=False): 635 | """ 636 | 撤回消息 637 | :param channel_id: 频道ID 638 | :param message_id: 消息ID 639 | :param hidetip: 隐藏提示小灰条 640 | :return: 成功返回空字符串, 失败返回错误信息 641 | """ 642 | url = f"{self.base_api}/channels/{channel_id}/messages/{message_id}" 643 | payload = { 644 | 'hidetip': str(hidetip).lower() 645 | } 646 | response = requests.request("DELETE", url, headers=self.__headers, params=payload) 647 | if response.status_code != 200: 648 | data = response.text 649 | self._tlogger(f"撤回消息失败: {data}", error_resp=response.text, 650 | traceid=response.headers.get("X-Tps-trace-ID")) 651 | return data 652 | else: 653 | return "" 654 | 655 | def api_get_api_permission(self, guild_id, retstr=False) -> t.Union[structs.APIPermission, str]: 656 | """ 657 | 获取频道可用权限列表 658 | :param guild_id: 频道ID 659 | :param retstr: 强制返回文本 660 | :return: 频道权限列表 661 | """ 662 | url = f"{self.base_api}/guilds/{guild_id}/api_permission" 663 | response = requests.request("GET", url, headers=self.__headers) 664 | return self._retter(response, "获取频道可用权限列表失败", structs.APIPermission, retstr, data_type=1) 665 | 666 | def api_demand_api_permission(self, guild_id, channel_id: str, path: str, method: str, desc: str, retstr=False) \ 667 | -> t.Union[structs.APIPermissionDemand, str]: 668 | """ 669 | 创建频道 API 接口权限授权链接 670 | :param guild_id: 频道ID 671 | :param channel_id: 子频道ID 672 | :param path: API 接口名,例如 /guilds/{guild_id}/members/{user_id} 673 | :param method: 请求方法,例如 GET 674 | :param desc: 机器人申请对应的 API 接口权限后可以使用功能的描述 675 | :param retstr: 强制返回文本 676 | :return: 接口权限需求对象 677 | """ 678 | url = f"{self.base_api}/guilds/{guild_id}/api_permission/demand" 679 | payload = { 680 | "channel_id": channel_id, 681 | "api_identify": { 682 | "path": path, 683 | "method": method 684 | }, 685 | "desc": desc 686 | } 687 | response = requests.request("POST", url, headers=self.__headers, data=json.dumps(payload)) 688 | return self._retter(response, "创建授权链接失败", structs.APIPermissionDemand, retstr, data_type=0) 689 | 690 | def api_add_pins(self, channel_id, message_id, retstr=False) -> t.Union[structs.PinsMessage, str]: 691 | """ 692 | 添加精华消息 693 | :param channel_id: 子频道ID 694 | :param message_id: 消息ID 695 | :param retstr: 强制返回str 696 | :return: 697 | """ 698 | url = f"{self.base_api}/channels/{channel_id}/pins/{message_id}" 699 | response = requests.request("PUT", url, headers=self.__headers) 700 | return self._retter(response, "添加精华消息失败", structs.PinsMessage, retstr, data_type=0) 701 | 702 | def api_remove_pins(self, channel_id, message_id): 703 | """ 704 | 移除精华消息 705 | :param channel_id: 子频道ID 706 | :param message_id: 消息ID, 删除全部填入: all 707 | :return: 708 | """ 709 | url = f"{self.base_api}/channels/{channel_id}/pins/{message_id}" 710 | response = requests.request("DELETE", url, headers=self.__headers) 711 | if response.status_code != 204: 712 | self._tlogger(f"移除精华消息失败: {response.text}", error=True, error_resp=response.text, 713 | traceid=response.headers.get("X-Tps-trace-ID")) 714 | return response.text 715 | else: 716 | return "" 717 | 718 | def api_get_pins(self, channel_id, retstr=False) -> t.Union[structs.PinsMessage, str]: 719 | """ 720 | 获取精华消息 721 | :param channel_id: 子频道ID 722 | :param retstr: 强制返回str 723 | :return: 724 | """ 725 | url = f"{self.base_api}/channels/{channel_id}/pins" 726 | response = requests.request("GET", url, headers=self.__headers) 727 | return self._retter(response, "获取精华消息失败", structs.PinsMessage, retstr, data_type=0) 728 | 729 | def api_send_message_reactions(self, channel_id, message_id, emoji_type, emoji_id): 730 | """ 731 | 发表表情表态 732 | :param channel_id: 子频道ID 733 | :param message_id: 消息ID 734 | :param emoji_type: 表情类型, 参考: https://bot.q.qq.com/wiki/develop/api/openapi/emoji/model.html#emojitype 735 | :param emoji_id: 表情列表, 参考: https://bot.q.qq.com/wiki/develop/api/openapi/emoji/model.html#Emoji%20%E5%88%97%E8%A1%A8 736 | :return: 成功返回空字符串 737 | """ 738 | url = f"{self.base_api}/channels/{channel_id}/messages/{message_id}/reactions/{emoji_type}/{emoji_id}" 739 | response = requests.request("PUT", url, headers=self.__headers) 740 | if response.status_code != 204: 741 | self._tlogger(f"发送表情表态失败: {response.text}", error=True, error_resp=response.text, 742 | traceid=response.headers.get("X-Tps-trace-ID")) 743 | return response.text 744 | else: 745 | return "" 746 | 747 | def api_pv_get_member_list(self, guild_id, retstr=False) -> t.Union[str, t.List[structs.Member], None]: 748 | """ 749 | 仅私域机器人可用 - 取频道成员列表 750 | :param guild_id: 频道ID 751 | :param retstr: 强制返回字符串 752 | :return: 成功返回成员列表, 失败返回错误信息 753 | """ 754 | url = f"{self.base_api}/guilds/{guild_id}/members" 755 | response = requests.request("GET", url, headers=self.__headers) 756 | return self._retter(response, "获取频道成员列表失败", structs.Member, retstr, data_type=1) 757 | 758 | def api_pv_kick_member(self, guild_id, user_id, add_blick_list=False, delete_history_msg_days=0) -> str: 759 | """ 760 | 仅私域机器人可用 - 踢出指定成员 761 | 消息撤回时间范围仅支持固定的天数:3,7,15,30。 特殊的时间范围:-1: 撤回全部消息。默认值为0不撤回任何消息。 762 | :param guild_id: 频道ID 763 | :param user_id: 成员ID 764 | :param add_blick_list: 将该成员加入黑名单 765 | :param delete_history_msg_days: 撤回该成员的消息 766 | :return: 成功返回空字符串, 失败返回错误信息 767 | """ 768 | url = f"{self.base_api}/guilds/{guild_id}/members/{user_id}" 769 | payload = { 770 | "add_blacklist": add_blick_list, 771 | "delete_history_msg_days": f"{delete_history_msg_days}" 772 | } 773 | response = requests.request("DELETE", url, headers=self.__headers, data=json.dumps(payload)) 774 | if response.status_code != 204: 775 | self._tlogger(f"移除成员失败: {response.text}", error=True, error_resp=response.text, 776 | traceid=response.headers.get("X-Tps-trace-ID")) 777 | return response.text 778 | else: 779 | return "" 780 | 781 | def api_pv_create_channel(self, guild_id, channel_name: str, channel_type: int, 782 | channel_position: int, channel_parent_id: int, retstr=False) \ 783 | -> t.Union[str, structs.Channel, None]: 784 | """ 785 | 仅私域机器人可用 - 创建子频道 786 | :param guild_id: 频道ID 787 | :param channel_name: 子频道名称 788 | :param channel_type: 子频道类型 789 | :param channel_position: 子频道排序 790 | :param channel_parent_id: 子频道分组ID 791 | :param retstr: 强制返回纯文本 792 | :return: 成功返回子频道信息, 失败返回错误信息 793 | """ 794 | url = f"{self.base_api}/guilds/{guild_id}/channels" 795 | body_s = { 796 | "name": channel_name, 797 | "type": channel_type, 798 | "position": channel_position, 799 | "parent_id": channel_parent_id 800 | } 801 | response = requests.request("POST", url, data=json.dumps(body_s), headers=self.__headers) 802 | return self._retter(response, "创建子频道失败", structs.Channel, retstr, data_type=0) 803 | 804 | def api_pv_change_channel(self, channel_id, channel_name: str, channel_type: int, 805 | channel_position: int, channel_parent_id: int, retstr=False) \ 806 | -> t.Union[str, structs.Channel, None]: 807 | """ 808 | 仅私域机器人可用 - 修改子频道信息 809 | :param channel_id: 子频道ID 810 | :param channel_name: 子频道名称 811 | :param channel_type: 子频道类型 812 | :param channel_position: 子频道排序 813 | :param channel_parent_id: 子频道分组ID 814 | :param retstr: 强制返回纯文本 815 | :return: 成功返回修改后的子频道信息, 失败返回错误信息 816 | """ 817 | url = f"{self.base_api}/channels/{channel_id}" 818 | body_s = { 819 | "name": channel_name, 820 | "type": channel_type, 821 | "position": channel_position, 822 | "parent_id": channel_parent_id 823 | } 824 | response = requests.request("PATCH", url, data=json.dumps(body_s), headers=self.__headers) 825 | return self._retter(response, "修改子频道失败", structs.Channel, retstr, data_type=0) 826 | 827 | def api_pv_delete_channel(self, channel_id): 828 | """ 829 | 仅私域机器人可用 - 删除子频道 830 | :param channel_id: 子频道ID 831 | :return: 成功返回空字符串, 失败返回错误信息 832 | """ 833 | url = f"{self.base_api}/channels/{channel_id}" 834 | response = requests.request("DELETE", url, headers=self.__headers) 835 | if response.status_code != 200 and response.status_code != 204: 836 | self._tlogger(f"删除子频道失败: {response.text}", error_resp=response.text, 837 | traceid=response.headers.get("X-Tps-trace-ID")) 838 | return response.text 839 | else: 840 | return "" 841 | 842 | def get_guild_info(self, guild_id, retstr=False) -> t.Union[str, structs.Guild, None]: 843 | url = f"{self.base_api}/guilds/{guild_id}" 844 | response = requests.request("GET", url, headers=self.__headers) 845 | return self._retter(response, "获取频道信息失败", structs.Guild, retstr, data_type=0) 846 | 847 | def get_guild_user_info(self, guild_id, member_id, retstr=False) -> t.Union[str, structs.Member, None]: 848 | url = f"{self.base_api}/guilds/{guild_id}/members/{member_id}" 849 | response = requests.request("GET", url, headers=self.__headers) 850 | return self._retter(response, "获取成员信息失败", structs.Member, retstr, data_type=0) 851 | 852 | def get_channel_info(self, channel_id, retstr=False) -> t.Union[str, structs.Channel, None]: 853 | url = f"{self.base_api}/channels/{channel_id}" 854 | response = requests.request("GET", url, headers=self.__headers) 855 | return self._retter(response, "获取子频道信息失败", structs.Channel, retstr, data_type=0) 856 | 857 | def get_guild_channel_list(self, guild_id, retstr=False) -> t.Union[str, t.List[structs.Channel], None]: 858 | url = f"{self.base_api}/guilds/{guild_id}/channels" 859 | response = requests.request("GET", url, headers=self.__headers) 860 | return self._retter(response, "获取子频道列表失败", structs.Channel, retstr, data_type=1) 861 | 862 | def get_message(self, channel_id, message_id, retstr=False) -> t.Union[str, structs.Message, None]: 863 | url = f"{self.base_api}/channels/{channel_id}/messages/{message_id}" 864 | response = requests.request("GET", url, headers=self.__headers) 865 | return self._retter(response, "获取消息信息失败", structs.Message, retstr, data_type=0) 866 | 867 | def get_self_info(self, use_cache=False) -> t.Union[str, structs.User, None]: 868 | if use_cache and "self_info" in self._cache: 869 | get_response = self._cache["self_info"] 870 | else: 871 | url = f"{self.base_api}/users/@me" 872 | response = requests.request("GET", url, headers=self.__headers) 873 | get_response = response.text 874 | self._cache["self_info"] = response.text 875 | 876 | data = json.loads(get_response) 877 | if "code" in data: 878 | self._tlogger(f"获取自身信息失败: {get_response}", error=True, error_resp=get_response) 879 | return None 880 | elif self.api_return_pydantic: 881 | data["bot"] = True 882 | return structs.User(**data) 883 | else: 884 | return get_response 885 | 886 | def get_self_guilds(self, before="", after="", limit="100", use_cache=False, retstr=False) \ 887 | -> t.Union[str, t.List[structs.Guild], None]: 888 | if use_cache and "get_self_guilds" in self._cache: 889 | get_response = self._cache["get_self_guilds"] 890 | x_trace_id = None 891 | else: 892 | if after != "": 893 | url = f"{self.base_api}/users/@me/guilds?after={after}&limit={limit}" 894 | elif before != "": 895 | url = f"{self.base_api}/users/@me/guilds?before={before}&limit={limit}" 896 | else: 897 | url = f"{self.base_api}/users/@me/guilds?limit={limit}" 898 | response = requests.request("GET", url, headers=self.__headers) 899 | get_response = response.text 900 | x_trace_id = response.headers.get("X-Tps-trace-ID") 901 | self._cache["get_self_guilds"] = response.text 902 | 903 | data = json.loads(get_response) 904 | if "code" in data: 905 | self._tlogger(f"获取频道列表失败: {get_response}", error=True, error_resp=get_response, traceid=x_trace_id) 906 | if retstr: 907 | return get_response 908 | return None 909 | elif self.api_return_pydantic and not retstr: 910 | return [structs.Guild(**g) for g in data] 911 | else: 912 | return get_response 913 | 914 | def api_get_guild_message_freq(self, guild_id, retstr=False) -> t.Union[str, None, structs.MessageSetting]: 915 | """ 916 | 获取频道消息频率设置 917 | :param guild_id: 频道ID 918 | :param retstr: 强制返回文本 919 | :return: MessageSetting 920 | """ 921 | url = f"{self.base_api}/guilds/{guild_id}/message/setting" 922 | response = requests.request("GET", url, headers=self.__headers) 923 | return self._retter(response, "获取频道消息频率设置失败", structs.MessageSetting, retstr, data_type=0) 924 | 925 | def api_send_message_guide(self, channel_id, content: str): 926 | """ 927 | 发送消息设置引导 928 | :param channel_id: 子频道ID 929 | :param content: 内容 930 | :return: 931 | """ 932 | url = f"{self.base_api}/channels/{channel_id}/settingguide" 933 | data = {"content": content} 934 | response = requests.request("POST", url, data=json.dumps(data), headers=self.__headers) 935 | if not str(response.status_code).startswith("2"): 936 | self._tlogger(f"发送消息设置引导失败: {response.text}", error=True, error_resp=response.text, 937 | traceid=response.headers.get("X-Tps-trace-ID")) 938 | else: 939 | return "" 940 | 941 | def _request_guild_role_member(self, guild_id, role_id, user_id, channel_id="", request_function="PUT"): 942 | url = f"{self.base_api}/guilds/{guild_id}/members/{user_id}/roles/{role_id}" 943 | body = {"channel": {"id": channel_id}} if channel_id != "" else None 944 | response = requests.request(request_function, url, data=None if body is None else json.dumps(body), 945 | headers=self.__headers) 946 | if response.status_code != 204: 947 | self._tlogger(f"{'增加' if request_function == 'PUT' else '删除'}频道身份组成员失败: {response.text}", error=True, 948 | error_resp=response.text, traceid=response.headers.get("X-Tps-trace-ID")) 949 | return response.text 950 | else: 951 | return "" 952 | 953 | # TODO 更多API 954 | -------------------------------------------------------------------------------- /bot_api/inter.py: -------------------------------------------------------------------------------- 1 | from . import structs 2 | from typing import Callable, List, Dict 3 | from . import api 4 | 5 | BCd = structs.Codes 6 | 7 | class BotMessageDistributor(api.BotApi): 8 | def __init__(self, appid: int, token: str, secret: str, sandbox: bool, debug=False, api_return_pydantic=False, 9 | output_log=True, log_path="", raise_api_error=False): 10 | self.debug = debug 11 | super().__init__(appid=appid, token=token, secret=secret, debug=debug, sandbox=sandbox, 12 | api_return_pydantic=api_return_pydantic, output_log=output_log, log_path=log_path, 13 | raise_api_error=raise_api_error) 14 | 15 | self.known_events = {BCd.SeverCode.BotGroupAtMessage: ["群艾特消息", structs.Message], 16 | BCd.SeverCode.AT_MESSAGE_CREATE: ["群艾特消息", structs.Message], 17 | BCd.SeverCode.DIRECT_MESSAGE_CREATE: ["私聊消息", structs.Message], 18 | BCd.SeverCode.MESSAGE_CREATE: ["收到消息(私域)", structs.Message], 19 | BCd.SeverCode.MESSAGE_DELETE: ["消息被撤回(私域)", structs.MessageDelete], 20 | BCd.SeverCode.GUILD_CREATE: ["Bot加入频道消息", structs.Guild], 21 | BCd.SeverCode.GUILD_UPDATE: ["频道更新", structs.Guild], 22 | BCd.SeverCode.GUILD_DELETE: ["频道更新", structs.Guild], 23 | BCd.SeverCode.CHANNEL_CREATE: ["子频道创建", structs.Channel], 24 | BCd.SeverCode.CHANNEL_UPDATE: ["子频道更新", structs.Channel], 25 | BCd.SeverCode.CHANNEL_DELETE: ["子频道删除", structs.Channel], 26 | BCd.SeverCode.GUILD_MEMBER_ADD: ["用户加入频道", structs.MemberWithGuildID], 27 | BCd.SeverCode.GUILD_MEMBER_UPDATE: ["用户信息更新", structs.MemberWithGuildID], 28 | BCd.SeverCode.GUILD_MEMBER_REMOVE: ["用户离开频道", structs.MemberWithGuildID], 29 | BCd.SeverCode.AUDIO_START: ["音频开始播放", structs.AudioAction], 30 | BCd.SeverCode.AUDIO_FINISH: ["音频结束", structs.AudioAction], 31 | BCd.SeverCode.AUDIO_ON_MIC: ["上麦", structs.AudioAction], 32 | BCd.SeverCode.AUDIO_OFF_MIC: ["下麦", structs.AudioAction], 33 | BCd.SeverCode.MESSAGE_REACTION_ADD: ["添加表情表态", structs.MessageReaction], 34 | BCd.SeverCode.MESSAGE_REACTION_REMOVE: ["移除表情表态", structs.MessageReaction], 35 | BCd.SeverCode.FUNC_CALL_AFTER_BOT_LOAD: ["Bot载入完成后加载函数", None], 36 | BCd.SeverCode.MESSAGE_AUDIT_PASS: ["消息审核通过", structs.MessageAudited], 37 | BCd.SeverCode.MESSAGE_AUDIT_REJECT: ["消息审核不通过", structs.MessageAudited], 38 | BCd.SeverCode.PUBLIC_MESSAGE_DELETE: ["消息被撤回(公域)", structs.MessageDelete], 39 | BCd.SeverCode.DIRECT_MESSAGE_DELETE: ["消息被撤回(私聊)", structs.MessageDelete] 40 | } 41 | 42 | self.bot_events: Dict[str, List] = {} 43 | 44 | self.img_to_url = None # 图片转为url 45 | 46 | self.modules = {} # 模块注册 47 | 48 | def receiver(self, reg_type: str, reg_name=""): 49 | self.logger(f"注册函数: {reg_type} {reg_name}", debug=True) 50 | 51 | def reg(func: Callable): 52 | def _adder(event_type, event_name): 53 | if event_type not in self.bot_events: 54 | self.bot_events[event_type] = [] 55 | self.bot_events[event_type].append(func) 56 | self.logger(f"函数: {func.__name__} 注册到事件: {event_name}", debug=True) 57 | 58 | if reg_type in self.known_events: 59 | _adder(reg_type, self.known_events[reg_type][0]) 60 | 61 | elif reg_type == BCd.SeverCode.image_to_url: 62 | if self.img_to_url is not None: 63 | self.logger(f"图片转url只允许注册一次。 生效函数由 {self.img_to_url.__name__} 更改为 {func.__name__}", 64 | warning=True) 65 | self.img_to_url = func 66 | 67 | elif reg_type == BCd.SeverCode.Module: # 注册自定义模块 68 | if reg_name != "": 69 | self.modules[reg_name] = func 70 | 71 | return reg 72 | 73 | def call_module(self, module, *args, **kwargs): # 调用模块 74 | return self.modules[module](*args, **kwargs) 75 | -------------------------------------------------------------------------------- /bot_api/logger.py: -------------------------------------------------------------------------------- 1 | import time 2 | from colorama import init 3 | import os 4 | 5 | init(autoreset=True) 6 | 7 | 8 | class Style: 9 | DEFAULT = 0 10 | BOLD = 1 11 | ITALIC = 3 12 | UNDERLINE = 4 13 | ANTIWHITE = 7 14 | 15 | 16 | class Color: 17 | DEFAULT = 39 18 | BLACK = 30 19 | RED = 31 20 | GREEN = 32 21 | YELLOW = 33 22 | BLUE = 34 23 | PURPLE = 35 24 | CYAN = 36 25 | WHITE = 37 26 | LIGHTBLACK_EX = 90 27 | LIGHTRED_EX = 91 28 | LIGHTGREEN_EX = 92 29 | LIGHTYELLOW_EX = 93 30 | LIGHTBLUE_EX = 94 31 | LIGHTMAGENTA_EX = 95 32 | LIGHTCYAN_EX = 96 33 | LIGHTWHITE_EX = 97 34 | 35 | 36 | class BGColor: 37 | DEFAULT = 49 38 | BLACK = 40 39 | RED = 41 40 | GREEN = 42 41 | YELLOW = 43 42 | BLUE = 44 43 | PURPLE = 45 44 | CYAN = 46 45 | WHITE = 47 46 | LIGHTBLACK_EX = 100 47 | LIGHTRED_EX = 101 48 | LIGHTGREEN_EX = 102 49 | LIGHTYELLOW_EX = 103 50 | LIGHTBLUE_EX = 104 51 | LIGHTMAGENTA_EX = 105 52 | LIGHTCYAN_EX = 106 53 | LIGHTWHITE_EX = 107 54 | 55 | 56 | class BotLogger: 57 | def __init__(self, debug: bool, write_out_log=True, log_path=""): 58 | self.debug = debug 59 | self.write_out_log = write_out_log 60 | self._log_path = f"{os.path.split(__file__)[0]}/log" if log_path == "" else log_path 61 | self.logger(f"log路径为: {self._log_path}") 62 | 63 | def logger(self, msg, debug=False, warning=False, error=False): 64 | _tm = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 65 | if error: 66 | self._printout(f"[{_tm}][ERROR] {msg}", Color.RED) 67 | elif warning: 68 | self._printout(f"[{_tm}][WARNING] {msg}", Color.LIGHTYELLOW_EX) 69 | elif debug and self.debug: 70 | self._printout(f"[{_tm}][DEBUG] {msg}", Color.BLUE) 71 | elif not debug: 72 | self._printout(f"[{_tm}][INFO] {msg}") 73 | 74 | def _printout(self, content, color=Color.DEFAULT, bgcolor=BGColor.DEFAULT, style=Style.DEFAULT): 75 | print("\033[{};{};{}m{}\033[0m".format(style, color, bgcolor, content)) 76 | if self.write_out_log: 77 | self._write_log(content) 78 | 79 | def _write_log(self, content): 80 | _tm = time.strftime("%Y-%m-%d", time.localtime()) 81 | try: 82 | with open(f"{self._log_path}/{_tm}.log", "a", encoding="utf8") as f: 83 | f.write(f"{content}\n") 84 | except FileNotFoundError: 85 | os.makedirs(f"{self._log_path}") 86 | self.logger("未找到log文件夹, 尝试重新创建", warning=True) 87 | -------------------------------------------------------------------------------- /bot_api/models.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | import json 3 | 4 | 5 | class BotCallingAPIError(Exception): 6 | def __init__(self, error_response: str, error_message="", x_tps_trace_id=None): 7 | try: 8 | data = json.loads(error_response) 9 | except json.JSONDecodeError: 10 | data = {} 11 | 12 | self.error_response = error_response 13 | self.error_code: t.Optional[int] = data["code"] if "code" in data else None 14 | self.error_description: str = data["message"] if "message" in data else str(error_response) 15 | self.error_message = error_message.replace(error_response, "").strip() 16 | self.x_tps_trace_id = x_tps_trace_id 17 | if self.error_message.endswith(":"): 18 | self.error_message = self.error_message[:-1] 19 | 20 | def __str__(self): 21 | return self.error_response 22 | 23 | 24 | class Embed: 25 | def __init__(self, title: str, fields: t.List[str], image_url: t.Optional[str] = None, 26 | prompt: t.Optional[str] = None): 27 | """ 28 | embed消息 29 | :param title: 标题 30 | :param fields: 内容文本 31 | :param image_url: 缩略图url, 选填 32 | :param prompt: 消息弹窗内容 33 | """ 34 | self.title = title 35 | self.prompt = prompt 36 | self.image_url = image_url 37 | self.fields = fields 38 | 39 | def ark_to_json(self): 40 | fields = [] 41 | for name in self.fields: 42 | fields.append({"name": name}) 43 | 44 | ret = { 45 | "embed": { 46 | "title": self.title, 47 | "fields": fields 48 | } 49 | } 50 | 51 | if self.prompt is not None: 52 | ret["embed"]["prompt"] = self.prompt 53 | if self.image_url is not None: 54 | ret["embed"]["thumbnail"] = {} 55 | ret["embed"]["thumbnail"]["url"] = self.image_url 56 | 57 | return ret 58 | 59 | 60 | class Ark: 61 | class LinkWithText: 62 | def __init__(self, description: str, prompt: str, text_and_link: t.Optional[t.List[t.List[str]]] = None): 63 | """ 64 | 23 链接+文本列表模板 65 | :param description: 描述 66 | :param prompt: 提示信息 67 | :param text_and_link: 正文, 可以混合使用纯文本/链接文本. 68 | 例1, 纯文本: [[文本], [文本], ...]; 例2: 链接文本: [[文本, 链接], [文本, 链接], ...] 69 | """ 70 | self.description = description 71 | self.prompt = prompt 72 | self.text_and_link = text_and_link 73 | 74 | def ark_to_json(self): 75 | ret = { 76 | "ark": { 77 | "template_id": 23, 78 | "kv": [ 79 | { 80 | "key": "#DESC#", 81 | "value": self.description 82 | }, 83 | { 84 | "key": "#PROMPT#", 85 | "value": self.prompt 86 | } 87 | ] 88 | } 89 | } 90 | if self.text_and_link is not None: 91 | k_obj = [] 92 | k_ds = ["desc", "link"] 93 | for obj in self.text_and_link: 94 | p_obj = {"obj_kv": []} 95 | count = 0 96 | for i in obj: 97 | p_obj["obj_kv"].append({ 98 | "key": k_ds[count], 99 | "value": i 100 | }) 101 | count += 1 102 | if count + 1 > len(obj): 103 | break 104 | if count != 0: 105 | k_obj.append(p_obj) 106 | obj_list = { 107 | "key": "#LIST#", 108 | "obj": k_obj 109 | } 110 | ret["ark"]["kv"].append(obj_list) 111 | return ret 112 | 113 | class TextAndThumbnail: 114 | def __init__(self, desc: str, prompt: str, title: str, metadesc: str, img_url: str, jump: str, subtitle: str): 115 | """ 116 | 24 文本+缩略图模板 117 | :param desc: 描述 118 | :param prompt: 提示文本 119 | :param title: 标题 120 | :param metadesc: 详细描述 121 | :param img_url: 图片链接 122 | :param jump: 跳转链接 123 | :param subtitle: 子标题(来源) 124 | """ 125 | self.desc = desc 126 | self.prompt = prompt 127 | self.title = title 128 | self.metadesc = metadesc 129 | self.img_url = img_url 130 | self.jump = jump 131 | self.subtitle = subtitle 132 | 133 | def ark_to_json(self): 134 | ret = { 135 | "ark": { 136 | "template_id": 24, 137 | "kv": [ 138 | { 139 | "key": "#DESC#", 140 | "value": self.desc 141 | }, 142 | { 143 | "key": "#PROMPT#", 144 | "value": self.prompt 145 | }, 146 | { 147 | "key": "#TITLE#", 148 | "value": self.title 149 | }, 150 | { 151 | "key": "#METADESC#", 152 | "value": self.metadesc 153 | }, 154 | { 155 | "key": "#IMG#", 156 | "value": self.img_url 157 | }, 158 | { 159 | "key": "#LINK#", 160 | "value": self.jump 161 | }, 162 | { 163 | "key": "#SUBTITLE#", 164 | "value": self.subtitle 165 | } 166 | ] 167 | } 168 | } 169 | return ret 170 | 171 | class BigImage: 172 | def __init__(self, prompt: str, title: str, sub_title: str, cover: str, jump: str): 173 | """ 174 | 37 大图模板 175 | :param prompt: 提示信息 176 | :param title: 标题 177 | :param sub_title: 子标题 178 | :param cover: 大图url, 尺寸: 975*540 179 | :param jump: 跳转链接 180 | """ 181 | self.prompt = prompt 182 | self.title = title 183 | self.sub_title = sub_title 184 | self.cover = cover 185 | self.jump = jump 186 | 187 | def ark_to_json(self): 188 | ret = { 189 | "ark": { 190 | "template_id": 37, 191 | "kv": [ 192 | { 193 | "key": "#PROMPT#", 194 | "value": self.prompt 195 | }, 196 | { 197 | "key": "#METATITLE#", 198 | "value": self.title 199 | }, 200 | { 201 | "key": "#METASUBTITLE#", 202 | "value": self.sub_title 203 | }, 204 | { 205 | "key": "#METACOVER#", 206 | "value": self.cover 207 | }, 208 | { 209 | "key": "#METAURL#", 210 | "value": self.jump 211 | } 212 | ] 213 | } 214 | } 215 | return ret 216 | 217 | def schedule_json(name, description, start_timestamp, end_timestamp, jump_channel_id, remind_type): 218 | ret = { 219 | "schedule": { 220 | "name": name, 221 | "description": description, 222 | "start_timestamp": start_timestamp, 223 | "end_timestamp": end_timestamp, 224 | "jump_channel_id": jump_channel_id, 225 | "remind_type": remind_type 226 | } 227 | } 228 | return ret 229 | 230 | # a = Ark.Embed("标题", ["内容1", "内容2"], "http", "xxtc").ark_to_json() 231 | # a = Ark.LinkWithText("描述", "提示", [["文本1"], ["文本2", "uurrll"]]).ark_to_json() 232 | 233 | def role_body(name="", color=-1, hoist=1): 234 | body = {"filter": {"name": 1 if name != "" else 0, 235 | "color": 1 if color != -1 else 0, 236 | "hoist": hoist}, 237 | "info": {"name": name, 238 | "color": color, 239 | "hoist": hoist}} 240 | if name == "": 241 | body["info"].pop("name") 242 | if color == -1: 243 | body["info"].pop("color") 244 | return body 245 | 246 | def audio_control(audio_url: str, status: int, text=""): 247 | ret = {"audio_url": audio_url, 248 | "text": text, 249 | "status": status} 250 | if status != 0 or text == "": 251 | ret.pop("text") 252 | return ret 253 | -------------------------------------------------------------------------------- /bot_api/sdk_main.py: -------------------------------------------------------------------------------- 1 | from . import inter 2 | from .structs import Codes as BCd 3 | import websocket 4 | import json 5 | import requests 6 | import time 7 | from threading import Thread 8 | import typing as t 9 | import os 10 | 11 | event_types = BCd.QBot.GatewayEventName 12 | 13 | 14 | class Intents: # https://bot.q.qq.com/wiki/develop/api/gateway/intents.html 15 | GUILDS = 1 << 0 16 | GUILD_MEMBERS = 1 << 1 17 | DIRECT_MESSAGE = 1 << 12 18 | AUDIO_ACTION = 1 << 29 19 | AT_MESSAGES = 1 << 30 20 | GUILD_MESSAGE_REACTIONS = 1 << 10 21 | FORUM_EVENT = 1 << 28 22 | MESSAGE_CREATE = 1 << 9 23 | MESSAGE_AUDIT = 1 << 27 24 | 25 | 26 | def on_new_thread(f): 27 | def task_qwq(*args, **kwargs): 28 | _t = Thread(target=f, args=args, kwargs=kwargs) 29 | _t.start() 30 | 31 | return (task_qwq) 32 | 33 | 34 | def get_connection(url, on_message, on_open, on_error, on_close): 35 | return websocket.WebSocketApp(url=url, 36 | on_message=on_message, 37 | on_open=on_open, 38 | on_error=on_error, 39 | on_close=on_close 40 | ) 41 | 42 | 43 | class BotApp(inter.BotMessageDistributor): 44 | def __init__(self, appid: int, token: str, secret: str, is_sandbox: bool, inters: t.List, 45 | debug=False, api_return_pydantic=False, ignore_at_self=False, output_log=True, log_path="", 46 | raise_api_error=False, call_on_load_event_every_reconnect=False): 47 | """ 48 | BotAPP 49 | :param appid: BotAPPId 50 | :param token: BotToken 51 | :param secret: BotSecret 52 | :param is_sandbox: 是否在测试环境运行 53 | :param inters: 接收事件 54 | :param debug: 输出debug日志 55 | :param api_return_pydantic: 调用api后返回pydantic对象, 默认为纯文本 56 | :param ignore_at_self: 过滤消息中艾特bot的内容 57 | :param output_log: 是否输出日志到本地 58 | :param log_path: 日志输出位置, 默认为sdk目录内log文件夹 59 | :param raise_api_error: 当API调用出错时, 抛出 BotCallingAPIError 异常 60 | :param call_on_load_event_every_reconnect: 每次触发重连都调用 FUNC_CALL_AFTER_BOT_LOAD 事件 61 | """ 62 | super().__init__(appid=appid, token=token, secret=secret, sandbox=is_sandbox, debug=debug, 63 | api_return_pydantic=api_return_pydantic, output_log=output_log, log_path=log_path, 64 | raise_api_error=raise_api_error) 65 | self.inters = inters 66 | self.ignore_at_self = ignore_at_self 67 | self.self_id = "" 68 | self.self_name = "" 69 | self.EVENT_MESSAGE_CREATE_CALL_AT_MESSAGE_CREATE = False 70 | self.session_id = None 71 | self._d = None # 心跳参数 72 | self._t = None 73 | self.heartbeat_time = -1 # 心跳间隔 74 | self.ws = None 75 | self._on_load_run = True 76 | self.call_on_load_event_every_reconnect = call_on_load_event_every_reconnect 77 | self._spath = os.path.split(__file__)[0] 78 | 79 | # @on_new_thread 80 | def start(self): 81 | websocket.enableTrace(False) 82 | while True: 83 | self.ws = get_connection(url=self._get_websocket_url(), 84 | on_message=self._on_message, 85 | on_open=self._on_open, 86 | on_error=self._ws_on_error, 87 | on_close=self._on_close) 88 | self.ws.run_forever() 89 | 90 | def _get_connection(self): 91 | ws = websocket.WebSocketApp(url=self._get_websocket_url(), 92 | on_message=self._on_message, 93 | on_open=self._on_open, 94 | on_error=self._ws_on_error, 95 | on_close=self._on_close 96 | ) 97 | return ws 98 | 99 | def _get_verify_body(self, reconnect=False): 100 | if reconnect: 101 | rb = { 102 | "op": 6, 103 | "d": { 104 | "token": f'Bot {self.appid}.{self.token}', 105 | "session_id": self.session_id, 106 | "seq": 1337 107 | } 108 | } 109 | else: 110 | rb = { 111 | "op": 2, 112 | "d": { 113 | "token": f'Bot {self.appid}.{self.token}', 114 | "intents": self._get_inters_code(), # 1073741827 115 | "shard": [0, 1], 116 | "properties": { 117 | "$os": "linux", 118 | "$browser": "python_sdk", 119 | "$device": "server" 120 | } 121 | } 122 | } 123 | 124 | return rb 125 | 126 | def _ws_on_error(self, ws, err, *args): 127 | try: 128 | raise err 129 | except websocket.WebSocketConnectionClosedException: 130 | self._on_close() 131 | 132 | self.logger(f"Error: {args}", error=True) 133 | 134 | def _on_message(self, ws, msg): # 收到ws消息 135 | try: 136 | self.logger(msg, debug=True) 137 | data = json.loads(msg) 138 | stat_code = data["op"] # 状态码, 参考: https://bot.q.qq.com/wiki/develop/api/gateway/opcode.html 139 | 140 | if stat_code == BCd.QBot.OPCode.Hello: # 网关下发的第一条消息 141 | if "heartbeat_interval" in data["d"]: # 初始化心跳 142 | self.heartbeat_time = data["d"]["heartbeat_interval"] / 1000 143 | ws.send(json.dumps(self._get_verify_body())) 144 | 145 | elif stat_code == BCd.QBot.OPCode.Dispatch: # 服务器主动推送消息 146 | self._d = data["s"] 147 | if "t" in data: 148 | s_type = data["t"] 149 | 150 | def _send_event(m_dantic, changed_data=None, changed_s_type=None): 151 | """ 152 | 事件聚合分发 153 | :param m_dantic: 要发送的消息(Basemodel) 154 | :param changed_data: 需要修改的消息, 默认为data["d"] 155 | :param changed_s_type: 需要修改的消息类型, 默认为s_type, 即data["t"] 156 | :return: 157 | """ 158 | _send_data = data["d"] if changed_data is None else changed_data 159 | _send_type = s_type if changed_s_type is None else changed_s_type 160 | self._event_handout(_send_type, m_dantic(**_send_data)) 161 | 162 | if s_type == event_types.READY: # 验证完成 163 | self.session_id = data["d"]["session_id"] 164 | self._on_open(data["d"]["user"]["id"], data["d"]["user"]["username"], data["d"]["user"]["bot"], 165 | is_login=True) 166 | self.send_heart_beat() 167 | 168 | elif s_type == event_types.RESUMED: # 服务器通知重连 169 | self.logger("重连完成, 事件已全部补发") 170 | 171 | elif s_type in self.known_events: 172 | s_dantic = self.known_events[s_type][1] 173 | if s_dantic is not None: 174 | if s_type in [event_types.AT_MESSAGE_CREATE, event_types.MESSAGE_CREATE, 175 | event_types.DIRECT_MESSAGE_CREATE]: 176 | if self.ignore_at_self: 177 | data["d"]["content"] = data["d"]["content"].replace(f"<@!{self.self_id}>", 178 | "").strip() 179 | if s_type == event_types.MESSAGE_CREATE: # 收到消息(私域) 180 | if self.EVENT_MESSAGE_CREATE_CALL_AT_MESSAGE_CREATE: # 普通消息依旧调用艾特消息函数 181 | _send_event(self.known_events[event_types.AT_MESSAGE_CREATE][1], 182 | changed_s_type=event_types.AT_MESSAGE_CREATE) 183 | if s_type in [event_types.MESSAGE_CREATE, event_types.AT_MESSAGE_CREATE]: # 群消息 184 | data["d"]["message_type_sdk"] = "guild" 185 | elif s_type == event_types.DIRECT_MESSAGE_CREATE: # 私聊 186 | data["d"]["message_type_sdk"] = "private" 187 | 188 | _send_event(s_dantic) 189 | 190 | # TODO 主题相关事件 191 | 192 | else: 193 | self.logger(f"收到未知或暂不支持的推送消息: {data}", debug=True) 194 | 195 | 196 | elif stat_code == BCd.QBot.OPCode.Reconnect: # 服务器通知重连 197 | self.logger("服务器通知重连") 198 | self._on_close() 199 | 200 | elif stat_code == BCd.QBot.OPCode.Invalid_Session: # 验证失败 201 | self.logger("连接失败: Invalid Session", error=True) 202 | self._on_close(active_close=True) 203 | 204 | except Exception as sb: 205 | self.logger(sb, error=True) 206 | 207 | def _event_handout(self, func_type: str, *args, **kwargs): 208 | if func_type in self.bot_events: 209 | throw_func = self.bot_events[func_type] 210 | if throw_func: 211 | for f in throw_func: 212 | _t = Thread(target=f, args=args, kwargs=kwargs) 213 | _t.start() 214 | 215 | def _check_files(self): 216 | pass 217 | 218 | def _on_open(self, botid="", botname="", isbot="", is_login=False): 219 | if is_login: 220 | self.self_id = botid 221 | self.self_name = botname 222 | self.logger(f"开始运行:\nBotID: {botid}\nBotName: {botname}\nbot: {isbot}") 223 | self._check_files() 224 | if self._on_load_run or self.call_on_load_event_every_reconnect: 225 | self._event_handout(BCd.SeverCode.FUNC_CALL_AFTER_BOT_LOAD, self) 226 | self._on_load_run = False 227 | else: 228 | self.logger("开始尝试连接") 229 | 230 | def _on_close(self, *args, active_close=False): 231 | self.logger(f"on_close {args}", debug=True) 232 | try: 233 | self.heartbeat_time = -1 234 | self._d = None 235 | self.ws.close() 236 | except Exception as sb: 237 | self.logger(f"关闭连接失败: {sb}", error=True) 238 | 239 | if not active_close: 240 | self.heartbeat_time = -1 241 | self._d = None 242 | self.logger("连接断开, 尝试重连", warning=True) 243 | self.ws.keep_running = False 244 | """ 245 | self.ws = get_connection(url=self._get_websocket_url(), 246 | on_message=self._on_message, 247 | on_open=self._on_open, 248 | on_error=self._ws_on_error, 249 | on_close=self._on_close) 250 | self.ws.run_forever() 251 | self.ws.send(json.dumps(self._get_verify_body(reconnect=True))) 252 | """ 253 | else: 254 | self.logger("连接已断开", warning=True) 255 | 256 | def send_heart_beat(self): 257 | 258 | def _send_heart_beat(): 259 | try: 260 | while True: 261 | if self.heartbeat_time != -1 and self._d is not None: 262 | self.logger(f"发送心跳: {self._d}", debug=True) 263 | 264 | self.ws.send(json.dumps({"op": 1, "d": self._d})) 265 | time.sleep(self.heartbeat_time) 266 | else: 267 | time.sleep(1) 268 | continue 269 | except websocket.WebSocketConnectionClosedException: 270 | self.logger("发送心跳包失败", error=True) 271 | self._on_close() 272 | 273 | if self._t is None: 274 | self._t = Thread(target=_send_heart_beat) 275 | self._t.start() 276 | else: 277 | if not self._t.is_alive(): 278 | self._t = Thread(target=_send_heart_beat) 279 | self._t.start() 280 | 281 | def _get_websocket_url(self): 282 | url = f"{self.base_api}/gateway" 283 | headers = {'Authorization': f'Bot {self.appid}.{self.token}'} 284 | response = requests.request("GET", url, headers=headers) 285 | try: 286 | self.logger(f"获取服务器api: {response.json()['url']}", debug=True) 287 | return response.json()["url"] 288 | except Exception as sb: 289 | self.logger(f"获取服务器API失败 - {response.text}") 290 | print(sb) 291 | 292 | def _get_inters_code(self): 293 | if type(self.inters) != list: 294 | self.logger("事件订阅(inters)错误, 将使用默认值", error=True) 295 | return 1073741827 296 | 297 | result = 0 298 | for _int in self.inters: 299 | result = result | _int 300 | 301 | self.logger(f"intents计算结果: {result}", debug=True) 302 | return result 303 | -------------------------------------------------------------------------------- /bot_api/structs.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | import typing as t 3 | 4 | 5 | class Codes: 6 | class QBot: 7 | class OPCode: # see: https://bot.q.qq.com/wiki/develop/api/gateway/opcode.html 8 | Dispatch = 0 9 | Heartbeat = 1 10 | Identify = 2 11 | Resume = 6 12 | Reconnect = 7 13 | Invalid_Session = 9 14 | Hello = 10 15 | Heartbeat_Ack = 11 16 | 17 | class GatewayEventName: 18 | READY = "READY" 19 | RESUMED = "RESUMED" 20 | AT_MESSAGE_CREATE = "AT_MESSAGE_CREATE" # 收到艾特消息 21 | MESSAGE_CREATE = "MESSAGE_CREATE" # 收到消息, 仅私域机器人有效 22 | DIRECT_MESSAGE_CREATE = "DIRECT_MESSAGE_CREATE" # 收到私聊消息 23 | GUILD_CREATE = "GUILD_CREATE" # bot加入频道 24 | GUILD_UPDATE = "GUILD_UPDATE" # 频道信息更新 25 | GUILD_DELETE = "GUILD_DELETE" # 频道解散/bot被移除 26 | CHANNEL_CREATE = "CHANNEL_CREATE" # 子频道被创建 27 | CHANNEL_UPDATE = "CHANNEL_UPDATE" # 子频道信息变更 28 | CHANNEL_DELETE = "CHANNEL_DELETE" # 子频道被删除 29 | GUILD_MEMBER_ADD = "GUILD_MEMBER_ADD" # 新用户加入频道 30 | GUILD_MEMBER_UPDATE = "GUILD_MEMBER_UPDATE" # TODO - TX: 暂无 31 | GUILD_MEMBER_REMOVE = "GUILD_MEMBER_REMOVE" # 用户离开频道 32 | AUDIO_START = "AUDIO_START" # 音频开始播放 33 | AUDIO_FINISH = "AUDIO_FINISH" # 音频结束 34 | AUDIO_ON_MIC = "AUDIO_ON_MIC" # 机器人上麦 35 | AUDIO_OFF_MIC = "AUDIO_OFF_MIC" # 机器人下麦 36 | MESSAGE_REACTION_ADD = "MESSAGE_REACTION_ADD" # 添加表情表态 37 | MESSAGE_REACTION_REMOVE = "MESSAGE_REACTION_REMOVE" # 删除表情表态 38 | THREAD_CREATE = "THREAD_CREATE" # 当用户创建主题时 39 | THREAD_UPDATE = "THREAD_UPDATE" # 当用户更新主题时 40 | THREAD_DELETE = "THREAD_DELETE" # 当用户删除主题时 41 | POST_CREATE = "POST_CREATE" # 当用户创建帖子时 42 | POST_DELETE = "POST_DELETE" # 当用户删除帖子时 43 | REPLY_CREATE = "REPLY_CREATE" # 当用户回复评论时 44 | REPLY_DELETE = "REPLY_DELETE" # 当用户回复评论时 45 | MESSAGE_AUDIT_PASS = "MESSAGE_AUDIT_PASS" # 消息审核通过 46 | MESSAGE_AUDIT_REJECT = "MESSAGE_AUDIT_REJECT" # 消息审核不通过 47 | 48 | PUBLIC_MESSAGE_DELETE = "PUBLIC_MESSAGE_DELETE" # 消息撤回(公域) 49 | MESSAGE_DELETE = "MESSAGE_DELETE" # 消息撤回(私域) 50 | DIRECT_MESSAGE_DELETE = "DIRECT_MESSAGE_DELETE" # 消息撤回(私聊) 51 | 52 | class UserRole: 53 | member = "1" # 全体成员 54 | admin = "2" # 管理员 55 | sub_admin = "5" # 子频道管理员 56 | owner = "4" # 创建者 57 | 58 | class SeverCode(QBot.GatewayEventName): 59 | BotGroupAtMessage = "AT_MESSAGE_CREATE" 60 | Module = "Module" 61 | image_to_url = "image_to_url" 62 | # FUNC_CALL_BEFORE_BOT_LOAD = "FUNC_CALL_BEFORE_BOT_LOAD" 63 | FUNC_CALL_AFTER_BOT_LOAD = "FUNC_CALL_AFTER_BOT_LOAD" 64 | 65 | 66 | class User(BaseModel): # 用户对象 67 | id: str 68 | username: t.Optional[str] 69 | avatar: t.Optional[str] 70 | bot: t.Optional[bool] 71 | union_openid: t.Optional[str] 72 | union_user_account: t.Optional[str] 73 | 74 | 75 | class Member(BaseModel): 76 | user: t.Optional[User] 77 | nick: t.Optional[str] 78 | roles: t.Optional[t.List[str]] 79 | joined_at: t.Optional[str] 80 | deaf: t.Optional[bool] 81 | mute: t.Optional[bool] 82 | pending: t.Optional[bool] 83 | 84 | 85 | class MessageAttachment(BaseModel): 86 | url: str 87 | content_type: t.Optional[str] 88 | filename: t.Optional[str] 89 | height: t.Optional[int] 90 | width: t.Optional[int] 91 | id: t.Optional[str] 92 | size: t.Optional[int] 93 | 94 | 95 | class MessageEmbedField(BaseModel): 96 | name: t.Optional[str] 97 | value: t.Optional[str] 98 | 99 | 100 | class MessageEmbed(BaseModel): 101 | title: t.Optional[str] 102 | description: t.Optional[str] 103 | prompt: t.Optional[str] 104 | timestamp: t.Optional[str] 105 | fields: t.List[MessageEmbedField] 106 | 107 | 108 | class MessageArkObjKv(BaseModel): 109 | key: str 110 | value: str 111 | 112 | 113 | class MessageArkObj(BaseModel): 114 | obj_kv: t.List[MessageArkObjKv] 115 | 116 | 117 | class MessageArkKv(BaseModel): 118 | key: str 119 | value: str 120 | obj: t.List[MessageArkObj] 121 | 122 | 123 | class MessageArk(BaseModel): 124 | template_id: int 125 | kv: t.List[MessageArkKv] 126 | 127 | class MessageReference(BaseModel): 128 | message_id: str 129 | ignore_get_message_error: t.Optional[bool] 130 | 131 | class Message(BaseModel): # 消息对象 132 | id: str # 消息id 133 | channel_id: str 134 | guild_id: str 135 | content: t.Optional[str] 136 | timestamp: str 137 | edited_timestamp: t.Optional[str] 138 | mention_everyone: t.Optional[bool] 139 | author: User 140 | attachments: t.Optional[t.List[MessageAttachment]] 141 | embeds: t.Optional[t.List[MessageEmbed]] 142 | mentions: t.Optional[t.List[User]] 143 | member: t.Optional[Member] 144 | ark: t.Optional[MessageArk] 145 | seq: t.Optional[int] 146 | seq_in_channel: t.Optional[str] 147 | message_reference: t.Optional[MessageReference] 148 | message_type_sdk: t.Optional[str] 149 | 150 | 151 | class Guild(BaseModel): # 频道对象 152 | id: t.Optional[str] # GUILD_DELETE 事件为 None 153 | name: t.Optional[str] # GUILD_DELETE 事件为 None 154 | icon: t.Optional[str] 155 | owner_id: t.Optional[str] # GUILD_DELETE 事件为 None 156 | owner: t.Optional[bool] 157 | member_count: t.Optional[int] # GUILD_DELETE 事件为 None 158 | max_members: t.Optional[int] # GUILD_DELETE 事件为 None 159 | description: t.Optional[str] # GUILD_DELETE 事件为 None 160 | joined_at: t.Optional[str] 161 | 162 | 163 | class Channel(BaseModel): # 子频道对象 164 | id: t.Optional[str] 165 | guild_id: t.Optional[str] 166 | name: t.Optional[str] 167 | type: t.Optional[int] 168 | sub_type: t.Optional[int] 169 | position: t.Optional[int] 170 | parent_id: t.Optional[str] 171 | owner_id: t.Optional[str] 172 | last_message_id: t.Optional[str] 173 | nsfw: t.Optional[bool] # 这个字段, 可能为True吗? 174 | private_type: t.Optional[int] 175 | speak_permission: t.Optional[int] 176 | application_id: t.Optional[str] 177 | permissions: t.Optional[str] 178 | rate_limit_per_user: t.Optional[int] 179 | 180 | 181 | class MemberWithGuildID(BaseModel): 182 | guild_id: str 183 | user: t.Optional[User] 184 | nick: str 185 | roles: t.Optional[t.List[str]] 186 | joined_at: str 187 | 188 | 189 | class AudioControl(BaseModel): 190 | audio_url: str 191 | text: str 192 | status: int 193 | 194 | class AudioControlSTATUS: 195 | START = 0 196 | PAUSE = 1 197 | RESUME = 2 198 | STOP = 3 199 | 200 | class AudioAction(BaseModel): 201 | guild_id: str 202 | channel_id: str 203 | audio_url: t.Optional[str] 204 | text: t.Optional[str] 205 | 206 | class ReactionTarget(BaseModel): 207 | id: str 208 | type: int 209 | 210 | class Emoji(BaseModel): 211 | id: str 212 | type: int 213 | 214 | class MessageReaction(BaseModel): 215 | user_id: str 216 | guild_id: str 217 | channel_id: str 218 | target: ReactionTarget 219 | emoji: Emoji 220 | 221 | 222 | class Schedule(BaseModel): 223 | id: str 224 | name: str 225 | description: str 226 | start_timestamp: str 227 | end_timestamp: str 228 | creator: Member 229 | jump_channel_id: str 230 | remind_type: str 231 | 232 | 233 | class Role(BaseModel): # 身份组对象 234 | id: str # 身份组ID 235 | name: str # 名称 236 | color: int # ARGB的HEX十六进制颜色值转换后的十进制数值 237 | hoist: int # 是否在成员列表中单独展示: 0-否, 1-是 238 | number: int # 人数 239 | member_limit: int # 成员上限 240 | 241 | 242 | class MessageAudited(BaseModel): # 消息审核对象 243 | audit_id: str 244 | message_id: str 245 | guild_id: str 246 | channel_id: str 247 | audit_time: str 248 | create_time: str 249 | seq_in_channel: t.Optional[str] 250 | 251 | 252 | class RecommendChannel(BaseModel): 253 | channel_id: t.Optional[str] 254 | introduce: t.Optional[str] 255 | 256 | class Announces(BaseModel): # 公告 257 | guild_id: str 258 | channel_id: str 259 | message_id: str 260 | announces_type: t.Optional[int] 261 | recommend_channels: t.Optional[RecommendChannel] 262 | 263 | 264 | class ChannelPermissions(BaseModel): # 子频道权限对象 265 | channel_id: str 266 | user_id: t.Optional[str] 267 | role_id: t.Optional[str] 268 | permissions: str # https://bot.q.qq.com/wiki/develop/api/openapi/channel_permissions/model.html#permissions 269 | 270 | 271 | class RetModel: 272 | class GetGuildRole(BaseModel): 273 | guild_id: str 274 | roles: t.List[Role] 275 | role_num_limit: str 276 | 277 | class CreateGuildRole(BaseModel): 278 | role_id: str 279 | role: Role 280 | 281 | class ChangeGuildRole(BaseModel): 282 | guild_id: str 283 | role_id: str 284 | role: Role 285 | 286 | 287 | class APIPermission(BaseModel): 288 | path: str 289 | method: str 290 | desc: str 291 | auth_status: int 292 | 293 | 294 | class APIPermissionDemandIdentify(BaseModel): 295 | path: str 296 | method: str 297 | 298 | 299 | class APIPermissionDemand(BaseModel): 300 | guild_id: str 301 | channel_id: str 302 | api_identify: APIPermissionDemandIdentify 303 | title: str 304 | desc: str 305 | 306 | 307 | class DMS(BaseModel): 308 | guild_id: t.Optional[str] 309 | channel_id: t.Optional[str] 310 | create_time: t.Optional[str] 311 | 312 | class PinsMessage(BaseModel): 313 | guild_id: t.Optional[str] 314 | channel_id: t.Optional[str] 315 | message_ids: t.Optional[t.List[str]] 316 | 317 | class MessageDelete(BaseModel): 318 | class DeletedMessage(BaseModel): 319 | author: t.Optional[User] 320 | channel_id: t.Optional[str] 321 | guild_id: t.Optional[str] 322 | id: t.Optional[str] 323 | direct_message: t.Optional[bool] 324 | 325 | class OpUser(BaseModel): 326 | id: t.Optional[str] 327 | 328 | message: t.Optional[DeletedMessage] 329 | op_user: t.Optional[OpUser] 330 | 331 | 332 | class MessageSetting(BaseModel): 333 | disable_create_dm: t.Optional[str] 334 | disable_push_msg: t.Optional[str] 335 | channel_ids: t.Optional[t.List[str]] 336 | channel_push_max_num: t.Optional[int] 337 | -------------------------------------------------------------------------------- /bot_api/utils.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | class yaml_util: 5 | @staticmethod 6 | def read(yaml_path): 7 | """ 8 | 读取指定目录的yaml文件 9 | :param yaml_path 相对当前的yaml文件绝对路径 10 | :return: yaml 中的内容 11 | """ 12 | with open(yaml_path, "r", encoding="utf-8") as f: 13 | return yaml.safe_load(f) 14 | -------------------------------------------------------------------------------- /bot_main.py: -------------------------------------------------------------------------------- 1 | import bot_api 2 | from bot_api.utils import yaml_util 3 | 4 | 5 | token = yaml_util.read('config.yaml') 6 | bot = bot_api.BotApp(token['bot']['id'], token['bot']['token'], token['bot']['secret'], 7 | is_sandbox=True, debug=True, api_return_pydantic=True, 8 | inters=[bot_api.Intents.GUILDS, bot_api.Intents.AT_MESSAGES, bot_api.Intents.GUILD_MEMBERS]) 9 | 10 | 11 | @bot.receiver(bot_api.structs.Codes.SeverCode.BotGroupAtMessage) 12 | def get_at_message(chain: bot_api.structs.Message): 13 | bot.logger(f"收到来自频道:{chain.guild_id} 子频道: {chain.channel_id} " 14 | f"内用户: {chain.author.username}({chain.author.id}) 的消息: {chain.content} ({chain.id})") 15 | 16 | if "你好" in chain.content: 17 | bot.api_send_reply_message(chain.channel_id, chain.id, "hello world!") 18 | elif "test" in chain.content: 19 | bot.api_send_reply_message(chain.channel_id, chain.id, "chieri在哟~") 20 | elif "/echo" in chain.content: 21 | reply = chain.content[chain.content.find("/echo") + len("/echo"):].strip() 22 | 23 | bot.api_send_reply_message(chain.channel_id, chain.id, reply) 24 | 25 | 26 | 27 | bot.start() 28 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | bot: 2 | id: 123 # 机器人id 3 | token: 456 # 机器人token 4 | secret: 789 # 机器人secret -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import bot_api 2 | import server 3 | 4 | bot = bot_api.BotApp(123456, "你的bot token", "你的bot secret", 5 | is_sandbox=True, debug=True, api_return_pydantic=True, 6 | inters=[bot_api.Intents.GUILDS, bot_api.Intents.AT_MESSAGES, bot_api.Intents.GUILD_MEMBERS]) 7 | 8 | 9 | app = server.BotServer(bot, ip_call="127.0.0.1", port_call=11415, ip_listen="127.0.0.1", port_listen=1988, 10 | allow_push=False) 11 | 12 | # 开始注册事件, 可以选择需要的进行注册 13 | app.reg_bot_at_message() # 艾特消息事件 14 | app.reg_guild_member_add() # 成员增加事件 15 | app.reg_guild_member_remove() # 成员减少事件 16 | 17 | # 以下事件与onebot差别较大 18 | app.reg_guild_create() # Bot加入频道事件 19 | app.reg_guild_update() # 频道信息更新事件 20 | app.reg_guild_delete() # Bot离开频道/频道被解散事件 21 | app.reg_channel_create() # 子频道创建事件 22 | app.reg_channel_update() # 子频道信息更新事件 23 | app.reg_channel_delete() # 子频道删除事件 24 | 25 | @app.bot.receiver(bot_api.structs.Codes.SeverCode.image_to_url) # 注册一个图片转url方法 26 | def img_to_url(img_path: str): 27 | # 用处: 发送图片时, 使用图片cq码[CQ:image,file=]或[CQ:image,url=] 28 | # 装饰器作用为: 解析cq码中图片的路径(网络图片则下载到本地), 将绝对路径传给本函数, 自行操作后, 返回图片url, sdk将使用此url发送图片 29 | # 若不注册此方法, 则无法发送图片。 30 | print(img_path) 31 | return "https://你的图片url" 32 | 33 | # 注册事件结束 34 | 35 | 36 | app.listening_server_start() # HTTP API 服务器启动 37 | app.bot.start() # Bot启动 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | Flask 3 | waitress 4 | python-dateutil 5 | pydantic 6 | websocket-client 7 | colorama 8 | schedule 9 | pyyaml -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import * 2 | -------------------------------------------------------------------------------- /server/message_convert.py: -------------------------------------------------------------------------------- 1 | import time 2 | from bot_api import structs as msgs 3 | import dateutil.parser 4 | from . import tools 5 | import re 6 | from pydantic import BaseModel 7 | import os 8 | import requests 9 | 10 | 11 | def time2timestamp(time_iso8601: str): 12 | d = dateutil.parser.parse(time_iso8601).timestamp() 13 | return int(d) 14 | 15 | 16 | def cq_to_guild_text(msg: str, func_img_to_url, auto_escape=False): 17 | rmsg = msg[:] 18 | reply = re.findall("\\[CQ:reply,id=\\w*]", msg) 19 | if reply: # 替换回复 20 | reply_id = tools.gettext_between(rmsg, "[CQ:reply,id=", "]") 21 | rmsg = rmsg.replace(reply[0], "") 22 | else: 23 | reply_id = "" 24 | 25 | if auto_escape: 26 | return (rmsg, reply_id, "") 27 | 28 | at = re.findall("\\[CQ:at,qq=\\w*]", msg) 29 | for _a in at: # 替换艾特 30 | rmsg = rmsg.replace(_a, f"<@!{tools.gettext_between(_a, '[CQ:at,qq=', ']')}>") 31 | 32 | 33 | ret_img_url = "" 34 | spath = os.path.split(__file__)[0] 35 | img = re.findall("\\[CQ:image,[^]]*]", rmsg) 36 | img_url = "" 37 | img_path = "" 38 | for im in img: 39 | ps = tools.gettext_between(im, "[CQ:image,", "]") 40 | _file = re.findall("file=[^,]*", ps) # 查找file参数 41 | if _file: 42 | img_path = _file[0][len("file="):-1 if _file[0].endswith(',') else None] 43 | if img_path.startswith("http"): 44 | img_url = img_path 45 | elif img_path.startswith("file:///"): 46 | img_path = img_path[8:] 47 | 48 | _url = re.findall("url=[^,]*", ps) # 查找url参数 49 | if _url: 50 | img_url = _url[0][len("url="):-1 if _url[0].endswith(',') else None] 51 | break # 仅支持一张图 52 | 53 | for im in img: 54 | rmsg = rmsg.replace(im, "", 1) # 清除图片cq码 55 | 56 | if callable(func_img_to_url): 57 | if img_url != "": # 参数为url 58 | save_name = tools.generate_randstring() + ".jpg" 59 | if not os.path.isdir(f"{spath}/temp"): 60 | os.mkdir(f"{spath}/temp") 61 | with open(f"{spath}/temp/{save_name}", "wb") as f: 62 | f.write(requests.get(img_url).content) 63 | ret_img_url = func_img_to_url(f"{spath}/temp/{save_name}") 64 | 65 | elif img_path != "": # 参数为path 66 | ret_img_url = func_img_to_url(img_path) 67 | 68 | others_cq = re.findall("\\[CQ:[^]]*]", rmsg) 69 | for ocq in others_cq: 70 | rmsg = rmsg.replace(ocq, "", 1) # 去除其它不受支持的CQ码 71 | 72 | return (rmsg, reply_id, ret_img_url) 73 | 74 | 75 | def guild_at_msg_to_groupmsg(msg: msgs.Message, selfid) -> dict: 76 | """ 77 | 频道艾特消息转onebot 11 78 | 新增了一些字段: guild_id, channel_id 79 | :param msg: 原信息 80 | :param selfid: 自身id 81 | :return: 82 | """ 83 | 84 | def cq_code_convert(msgcontent: str): 85 | retmsg = msgcontent[:] 86 | at_msgs = re.findall("<@!\\d*>", msgcontent) 87 | for _at in at_msgs: 88 | retmsg = retmsg.replace(_at, f"[CQ:at,qq={tools.gettext_between(retmsg, '<@!', '>')}]", 1) 89 | 90 | return retmsg 91 | 92 | img_cq = "" 93 | if msg.attachments is not None: # 判断图片 94 | for _im in msg.attachments: 95 | if _im.content_type is not None: 96 | if "image" in _im.content_type: 97 | msg_url = _im.url 98 | if not msg_url.startswith("http"): 99 | msg_url = f"http://{msg_url}" 100 | img_cq += f"[CQ:image,url={msg_url}]" 101 | 102 | if msgs.Codes.QBot.UserRole.owner in msg.member.roles: 103 | userRole = "owner" 104 | elif msgs.Codes.QBot.UserRole.admin in msg.member.roles: 105 | userRole = "admin" 106 | elif msgs.Codes.QBot.UserRole.sub_admin in msg.member.roles: 107 | userRole = "admin" 108 | else: 109 | userRole = "member" 110 | 111 | retdata = { 112 | "time": time2timestamp(msg.timestamp), 113 | "self_id": selfid, 114 | "post_type": "message", 115 | "message_type": "group", 116 | "sub_type": "normal", 117 | "message_id": msg.id, 118 | "group_id": msg.channel_id, 119 | "channel_id": msg.channel_id, 120 | "guild_id": msg.guild_id, 121 | "self_tiny_id": selfid, 122 | "userguildid": msg.author.id, 123 | "user_id": msg.author.id, 124 | "anonymous": None, 125 | "message": cq_code_convert(msg.content) + img_cq, 126 | "raw_message": msg.content, 127 | "font": -1, 128 | "sender": { 129 | "user_id": msg.author.id, 130 | "nickname": msg.author.username, 131 | "card": msg.author.username, 132 | "sex": "Unknown", 133 | "age": 0, 134 | "area": "Unknown", 135 | "level": "", 136 | "role": userRole, 137 | "title": "" 138 | } 139 | } 140 | return retdata 141 | 142 | 143 | def self_info_convert_to_json(info: msgs.User): 144 | retdata = { 145 | "id": info.id, 146 | "username": info.username, 147 | "avatar": info.avatar 148 | } 149 | return retdata 150 | 151 | def guild_member_info_convert(info: msgs.Member, group_id): 152 | try: 153 | if msgs.Codes.QBot.UserRole.owner in info.roles: 154 | userRole = "owner" 155 | elif msgs.Codes.QBot.UserRole.admin in info.roles: 156 | userRole = "admin" 157 | elif msgs.Codes.QBot.UserRole.sub_admin in info.roles: 158 | userRole = "admin" 159 | else: 160 | userRole = "member" 161 | 162 | retdata = { 163 | "group_id": group_id, 164 | "user_id": info.user.id, 165 | "nickname": info.nick, 166 | "card": info.nick, 167 | "sex": "Unknown", 168 | "age": 0, 169 | "area": "", 170 | "join_time": 0, 171 | "last_sent_time": 0, 172 | "role": userRole, 173 | "unfriendly": False, 174 | "title": "", 175 | "title_expire_time": -1, 176 | "card_changeable": True, 177 | "avatar": info.user.avatar 178 | } 179 | return retdata 180 | 181 | except: 182 | retdata = str(info) 183 | return retdata 184 | 185 | def guild_member_change_convert(info: msgs.MemberWithGuildID, selfid, is_useradd=True): 186 | try: 187 | retdata = { 188 | "time": int(time.time()), 189 | "self_id": selfid, 190 | "post_type": "notice", 191 | "notice_type": "group_increase" if is_useradd else "group_decrease", 192 | "sub_type": "approve" if is_useradd else "leave", 193 | "group_id": info.guild_id, 194 | "user_id": info.user.id if info.user is not None else "", 195 | "operator_id": info.user.id if info.user is not None else "", 196 | } 197 | 198 | return retdata 199 | except Exception as sb: 200 | retdata = { 201 | "code": -114514, 202 | "msg": repr(sb) 203 | } 204 | return retdata 205 | 206 | def others_event(selfid, post_type, notice_type, sub_type, data: BaseModel, user_id="", guiid_id="", channel_id=""): 207 | 208 | retdata = { 209 | "time": int(time.time()), 210 | "self_id": selfid, 211 | "post_type": post_type, 212 | "notice_type": notice_type, 213 | "sub_type": sub_type, 214 | 215 | "user_id": user_id, 216 | "guiid_id": guiid_id, 217 | "channel_id": channel_id, 218 | "data": data.dict() 219 | } 220 | 221 | return retdata 222 | -------------------------------------------------------------------------------- /server/sender.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import bot_api 4 | from . import message_convert 5 | from . import tools 6 | import requests 7 | 8 | 9 | class MessageSender: 10 | def __init__(self, bot_app: bot_api.BotApp, ip_call: str, port_call: int): 11 | self.bot = bot_app 12 | self.ip_call = ip_call 13 | self.port_call = port_call 14 | 15 | 16 | @tools.on_new_thread 17 | def _send_request(self, msg: str, method="POST"): 18 | try: 19 | self.bot.logger(f"推送事件: {msg}", debug=True) 20 | headers = {'Content-Type': 'application/json'} 21 | requests.request(method=method, url=f"http://{self.ip_call}:{self.port_call}", data=msg, headers=headers) 22 | except Exception as sb: 23 | self.bot.logger(f"推送事件失败: {sb}", error=True) 24 | 25 | def reg_bot_at_message(self): 26 | """ 27 | 开启艾特消息Event 28 | """ 29 | self.bot.logger("艾特消息Event已开启") 30 | self.bot.receiver(bot_api.structs.Codes.SeverCode.BotGroupAtMessage)(self._get_at_message) 31 | 32 | def reg_guild_member_add(self): 33 | """ 34 | 开启成员增加Event 35 | """ 36 | self.bot.logger("成员增加Event已开启") 37 | self.bot.receiver(bot_api.structs.Codes.SeverCode.GUILD_MEMBER_ADD)(self._get_guild_member_change(True)) 38 | 39 | def reg_guild_member_remove(self): 40 | """ 41 | 开启成员减少Event 42 | """ 43 | self.bot.logger("成员减少Event已开启") 44 | self.bot.receiver(bot_api.structs.Codes.SeverCode.GUILD_MEMBER_REMOVE)(self._get_guild_member_change(False)) 45 | 46 | def reg_guild_create(self): 47 | """ 48 | 开启Bot加入频道Event 49 | """ 50 | self.bot.logger("Bot加入频道Event已开启") 51 | self.bot.receiver(bot_api.structs.Codes.SeverCode.GUILD_CREATE)(self._get_guild_change("guild_create")) 52 | 53 | def reg_guild_delete(self): 54 | """ 55 | 开启Bot离开频道Event 56 | """ 57 | self.bot.logger("Bot离开频道Event已开启") 58 | self.bot.receiver(bot_api.structs.Codes.SeverCode.GUILD_DELETE)(self._get_guild_change("guild_delete")) 59 | 60 | def reg_guild_update(self): 61 | """ 62 | 开启频道信息更新Event 63 | """ 64 | self.bot.logger("频道信息更新Event已开启") 65 | self.bot.receiver(bot_api.structs.Codes.SeverCode.GUILD_DELETE)(self._get_guild_change("guild_update")) 66 | 67 | def reg_channel_create(self): 68 | """ 69 | 开启子频道创建Event 70 | """ 71 | self.bot.logger("子频道创建Event已开启") 72 | self.bot.receiver(bot_api.structs.Codes.SeverCode.CHANNEL_CREATE)(self._get_channel_change("channel_create")) 73 | 74 | def reg_channel_update(self): 75 | """ 76 | 开启子频道更新Event 77 | """ 78 | self.bot.logger("子频道更新Event已开启") 79 | self.bot.receiver(bot_api.structs.Codes.SeverCode.CHANNEL_UPDATE)(self._get_channel_change("channel_update")) 80 | 81 | def reg_channel_delete(self): 82 | """ 83 | 开启子频道删除Event 84 | """ 85 | self.bot.logger("子频道删除Event已开启") 86 | self.bot.receiver(bot_api.structs.Codes.SeverCode.CHANNEL_DELETE)(self._get_channel_change("channel_delete")) 87 | 88 | 89 | def _get_channel_change(self, notice_type): 90 | def send_channel(event: bot_api.structs.Channel): 91 | converted_json = message_convert.others_event(selfid=self.bot.get_self_info(use_cache=True).id, 92 | post_type="notice", notice_type=notice_type, 93 | sub_type=notice_type, 94 | user_id="", guiid_id=event.guild_id, channel_id=event.id, 95 | data=event) 96 | self._send_request(json.dumps(converted_json)) 97 | return send_channel 98 | 99 | def _get_guild_change(self, notice_type): 100 | def send_guild(event: bot_api.structs.Guild): 101 | converted_json = message_convert.others_event(selfid=self.bot.get_self_info(use_cache=True).id, 102 | post_type="notice", notice_type=notice_type, 103 | sub_type=notice_type, 104 | user_id="", guiid_id=event.id, channel_id="", data=event) 105 | self._send_request(json.dumps(converted_json)) 106 | return send_guild 107 | 108 | def _get_guild_member_change(self, is_useradd: bool): 109 | def send_member_change(event: bot_api.structs.MemberWithGuildID): 110 | converted_json = message_convert.guild_member_change_convert(event, self.bot.get_self_info(use_cache=True).id, 111 | is_useradd=is_useradd) 112 | self._send_request(json.dumps(converted_json)) 113 | return send_member_change 114 | 115 | def _get_at_message(self, chain: bot_api.structs.Message): 116 | self.bot.logger(f"收到来自频道:{chain.guild_id} 子频道: {chain.channel_id} " 117 | f"内用户: {chain.author.username}({chain.author.id}) 的消息: {chain.content} ({chain.id})") 118 | 119 | converted_json = message_convert.guild_at_msg_to_groupmsg(chain, self.bot.get_self_info(use_cache=True).id) 120 | self._send_request(json.dumps(converted_json)) 121 | -------------------------------------------------------------------------------- /server/server.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import Flask, jsonify 3 | from flask import request 4 | from waitress import serve as wserve 5 | from . import sender 6 | from . import tools 7 | from . import message_convert 8 | import bot_api 9 | 10 | flask = Flask(__name__) 11 | 12 | 13 | class BotServer(sender.MessageSender): 14 | def __init__(self, bot_app: bot_api.BotApp, ip_call: str, port_call: int, ip_listen: str, port_listen: int, 15 | allow_push=False): 16 | """ 17 | 启动HTTP上报器 18 | :param bot_app: BotAPP 19 | :param ip_call: Event回报ip 20 | :param port_call: Event回报端口 21 | :param ip_listen: POST上报ip 22 | :param port_listen: POST上报端口 23 | :param allow_push: 是否允许发送主动推送消息(即消息内不含CQ码: [CQ:reply,id=...]) 24 | """ 25 | super().__init__(bot_app, ip_call, port_call) 26 | 27 | self.ip_listen = ip_listen 28 | self.port_listen = port_listen 29 | self.allow_push = allow_push 30 | self.cache = {} 31 | 32 | def send_group_msg(self): 33 | channel_id = request.args.get("group_id") 34 | message = request.args.get("message") 35 | auto_escape = request.args.get("auto_escape") 36 | 37 | cmsg, reply_msg_id, img_url = message_convert.cq_to_guild_text(message, self.bot.img_to_url, auto_escape) 38 | if not self.allow_push and reply_msg_id == "": 39 | self.bot.logger("不发送允许PUSH消息, 请添加回复id, 或者将\"allow_push\"设置为True", warning=True) 40 | else: 41 | sendmsg = self.bot.api_send_message(channel_id, reply_msg_id, cmsg, img_url, retstr=True) 42 | sdata = json.loads(sendmsg) 43 | if "id" in sdata: 44 | ret = { 45 | "data": { 46 | "message_id": sdata["id"] 47 | }, 48 | "retcode": 0, 49 | "status": "ok" 50 | } 51 | else: 52 | ret = sdata 53 | 54 | return jsonify(ret) 55 | 56 | def get_self_info(self): 57 | use_cache = request.args.get("cache") 58 | selfinfo = self.bot.get_self_info(use_cache) 59 | return jsonify(message_convert.self_info_convert_to_json(selfinfo)) 60 | 61 | def get_group_member_info(self): 62 | group_id = request.args.get("group_id") 63 | user_id = request.args.get("user_id") 64 | no_cache = request.args.get("no_cache") 65 | 66 | if not no_cache and f"get_group_member_info_{group_id}_{user_id}" in self.cache: 67 | data = self.cache[f"get_group_member_info_{group_id}_{user_id}"] 68 | else: 69 | data = message_convert.guild_member_info_convert(self.bot.get_guild_user_info(group_id, user_id), group_id) 70 | self.cache[f"get_group_member_info_{group_id}_{user_id}"] = data 71 | return jsonify(data) 72 | 73 | def get_channel_info(self): 74 | channel_id = request.args.get("channel_id") 75 | data = self.bot.get_channel_info(channel_id, retstr=True) 76 | return jsonify(json.loads(data)) 77 | 78 | def get_channel_list(self): 79 | guild_id = request.args.get("guild_id") 80 | data = self.bot.get_guild_channel_list(guild_id, retstr=True) 81 | return jsonify(json.loads(data)) 82 | 83 | def get_message(self): 84 | channel_id = request.args.get("channel_id") 85 | message_id = request.args.get("message_id") 86 | data = self.bot.get_message(channel_id, message_id, retstr=True) 87 | return jsonify(json.loads(data)) 88 | 89 | def get_self_guild_list(self): 90 | cache = request.args.get("cache") 91 | before = "" if request.args.get("before") is None else request.args.get("before") 92 | after = "" if request.args.get("after") is None else request.args.get("before") 93 | limit = 100 if request.args.get("limit") is None else request.args.get("limit") 94 | data = self.bot.get_self_guilds(before=before, after=after, limit=limit, use_cache=cache, retstr=True) 95 | return jsonify(json.loads(data)) 96 | 97 | def get_guild_info(self): 98 | guild_id = request.args.get("guild_id") 99 | data = self.bot.get_guild_info(guild_id, retstr=True) 100 | return jsonify(json.loads(data)) 101 | 102 | 103 | @staticmethod 104 | @flask.route("/mark_msg_as_read") 105 | def mark_msg_as_read(): 106 | return "ok" 107 | 108 | @tools.on_new_thread 109 | def listening_server_start(self): 110 | flask.route("/send_group_msg", methods=["GET", "POST"])(self.send_group_msg) 111 | flask.route("/get_self_info", methods=["GET", "POST"])(self.get_self_info) 112 | flask.route("/get_self_guild_list", methods=["GET", "POST"])(self.get_self_guild_list) 113 | flask.route("/get_group_member_info", methods=["GET", "POST"])(self.get_group_member_info) 114 | flask.route("/get_channel_info", methods=["GET", "POST"])(self.get_channel_info) 115 | flask.route("/get_channel_list", methods=["GET", "POST"])(self.get_channel_list) 116 | flask.route("/get_message", methods=["GET", "POST"])(self.get_message) 117 | flask.route("/get_guild_info", methods=["GET", "POST"])(self.get_guild_info) 118 | 119 | # flask.run(self.ip_listen, self.port_listen) 120 | self.bot.logger(f"Event回报地址: {self.ip_call}:{self.port_call}") 121 | self.bot.logger(f"POST上报器启动: {self.ip_listen}:{self.port_listen}") 122 | wserve(flask, host=self.ip_listen, port=self.port_listen) 123 | -------------------------------------------------------------------------------- /server/tools.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | import random 3 | import string 4 | 5 | 6 | def gettext_between(text: str, before: str, after: str, is_include=False) -> str: 7 | """ 8 | 取出中间文本 9 | :param text: 原文本 10 | :param before: 前面文本 11 | :param after: 后面文本 12 | :param is_include: 是否取出标识文本 13 | :return: 操作后的文本 14 | """ 15 | b_index = text.find(before) 16 | 17 | if b_index == -1: 18 | b_index = 0 19 | else: 20 | b_index += len(before) 21 | af_index = text.find(after, b_index) 22 | if af_index == -1: 23 | af_index = len(text) 24 | rettext = text[b_index: af_index] 25 | if is_include: 26 | rettext = before + rettext + after 27 | return rettext 28 | 29 | 30 | def on_new_thread(f): 31 | def task_qwq(*args, **kwargs): 32 | t = Thread(target=f, args=args, kwargs=kwargs) 33 | t.start() 34 | 35 | return (task_qwq) 36 | 37 | def generate_randstring(num=8): 38 | value = ''.join(random.sample(string.ascii_letters + string.digits, num)) 39 | return(value) 40 | --------------------------------------------------------------------------------