├── .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 |
--------------------------------------------------------------------------------