├── .flake8
├── .github
└── workflows
│ └── python-publish.yml
├── .gitignore
├── .orange-ci.yml
├── .pre-commit-config.yaml
├── .pylintrc
├── LICENSE
├── README.md
├── README.rst
├── botpy
├── __init__.py
├── api.py
├── audio.py
├── channel.py
├── client.py
├── connection.py
├── errors.py
├── ext
│ ├── __init__.py
│ ├── channel_jump
│ │ └── __init__.py
│ ├── cog_apscheduler
│ │ └── __init__.py
│ ├── cog_yaml
│ │ └── __init__.py
│ ├── command_util.py
│ └── convert_color
│ │ └── __init__.py
├── flags.py
├── forum.py
├── gateway.py
├── guild.py
├── http.py
├── interaction.py
├── logging.py
├── manage.py
├── message.py
├── reaction.py
├── robot.py
├── types
│ ├── __init__.py
│ ├── announce.py
│ ├── audio.py
│ ├── channel.py
│ ├── emoji.py
│ ├── forum.py
│ ├── gateway.py
│ ├── guild.py
│ ├── inline.py
│ ├── interaction.py
│ ├── message.py
│ ├── permission.py
│ ├── pins_message.py
│ ├── reaction.py
│ ├── rich_text.py
│ ├── robot.py
│ ├── schedule.py
│ ├── session.py
│ └── user.py
└── user.py
├── docs
├── 20211216-QQ频道机器人分享-qqbot-python(open).pdf
└── 事件监听.md
├── examples
├── README.md
├── config.example.yaml
├── demo_announce.py
├── demo_api_permission.py
├── demo_at_reply.py
├── demo_at_reply_ark.py
├── demo_at_reply_command.py
├── demo_at_reply_embed.py
├── demo_at_reply_file_data.py
├── demo_at_reply_keyboard.py
├── demo_at_reply_markdown.py
├── demo_at_reply_reference.py
├── demo_audio_or_live_channel_member.py
├── demo_c2c_manage_event.py
├── demo_c2c_reply_file.py
├── demo_c2c_reply_text.py
├── demo_dms_reply.py
├── demo_get_reaction_users.py
├── demo_group_manage_event.py
├── demo_group_reply_file.py
├── demo_group_reply_text.py
├── demo_guild_member_event.py
├── demo_open_forum_event.py
├── demo_pins_message.py
├── demo_recall.py
├── demo_schedule.py
└── resource
│ └── test.png
├── requirements.txt
├── setup.cfg
├── setup.py
└── tests
├── .test(demo).yaml
├── __init__.py
├── test_api.py
├── test_flags.py
└── test_token.py
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore =
3 | ;W503 line break before binary operator
4 | W503,
5 | ;E203 whitespace before ':'
6 | E203
7 |
8 | ; exclude file
9 | exclude =
10 | .tox,
11 | .git,
12 | __init__.py,
13 | __pycache__,
14 | build,
15 | dist,
16 | *.pyc,
17 | *.egg-info,
18 | .cache,
19 | .eggs
20 |
21 | max-line-length = 120
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | release:
13 | types: [published]
14 |
15 | jobs:
16 | deploy:
17 |
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Set up Python
23 | uses: actions/setup-python@v2
24 | with:
25 | python-version: '3.x'
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip
29 | pip install build
30 | - name: Build package
31 | run: python -m build
32 | - name: Publish package
33 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
34 | with:
35 | user: __token__
36 | password: ${{ secrets.PYPI_API_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # pytype static type analyzer
135 | .pytype/
136 |
137 | # Cython debug symbols
138 | cython_debug/
139 |
140 | # botpy
141 | config.y*ml
142 | tests/.test.yaml
143 | botpy.log.*
144 | temp/
145 |
146 | # idea config
147 | .idea
148 |
149 | # vscode config
150 | .vscode
--------------------------------------------------------------------------------
/.orange-ci.yml:
--------------------------------------------------------------------------------
1 | master:
2 | merge_request:
3 | # Python的CI检查
4 | - docker:
5 | image: python:3.7-slim-stretch # 通过此参数控制使用的镜像环境
6 | stages:
7 | - name: Start Python CI
8 | script:
9 | - echo "Python unit test started"
10 | - name: Install Requirements #安装项目所需要的pip 依赖
11 | script:
12 | - echo "start to install python requirements"
13 | - pip install -i https://mirrors.tencent.com/pypi/simple/ -r requirements.txt
14 | - name: Install Unitest Packages
15 | script:
16 | - echo "start to install Unitest Packages"
17 | - pip install -i https://mirrors.tencent.com/pypi/simple/ pylint
18 | - pip install -i https://mirrors.tencent.com/pypi/simple/ coverage
19 | - pip install -i https://mirrors.tencent.com/pypi/simple/ pytest
20 | - name: Code Style Test
21 | script:
22 | - find . -type f -name "*.py" | xargs pylint
23 | - name: Unitest
24 | script:
25 | - pytest
26 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/ambv/black
3 | rev: 23.11.0
4 | hooks:
5 | - id: black
6 | args: [-l 120]
7 | - repo: https://github.com/pre-commit/pre-commit-hooks
8 | rev: v1.2.3
9 | hooks:
10 | - id: flake8
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
3 | # number of processors available to use.
4 | jobs=1
5 |
6 |
7 | [MESSAGES CONTROL]
8 |
9 | # Disable the message, report, category or checker with the given id(s).
10 | disable=all
11 |
12 | # Enable the message, report, category or checker with the given id(s).
13 | enable=c-extension-no-member,
14 | bad-indentation,
15 | bare-except,
16 | broad-except,
17 | dangerous-default-value,
18 | function-redefined,
19 | len-as-condition,
20 | line-too-long,
21 | misplaced-future,
22 | missing-final-newline,
23 | mixed-line-endings,
24 | multiple-imports,
25 | multiple-statements,
26 | singleton-comparison,
27 | trailing-comma-tuple,
28 | trailing-newlines,
29 | trailing-whitespace,
30 | unexpected-line-ending-format,
31 | unused-import,
32 | unused-variable,
33 | wildcard-import,
34 | wrong-import-order
35 |
36 |
37 | [FORMAT]
38 |
39 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
40 | expected-line-ending-format=LF
41 |
42 | # Regexp for a line that is allowed to be longer than the limit.
43 | ignore-long-lines=^\s*(# )??$
44 |
45 | # Maximum number of characters on a single line.
46 | max-line-length=120
47 |
48 | # Maximum number of lines in a module.
49 | max-module-lines=2000
50 |
51 |
52 | [EXCEPTIONS]
53 |
54 | # Exceptions that will emit a warning when being caught. Defaults to
55 | # "BaseException, Exception".
56 | overgeneral-exceptions=BaseException,
57 | Exception
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tencent
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 |
2 |
3 | 
4 |
5 | [](https://www.python.org/)
6 | [](https://github.com/tencent-connect/botpy/blob/master/LICENSE)
7 | 
8 | 
9 | [](https://devops.woa.com/process/api-html/user/builds/projects/qq-guild-open/pipelines/p-713959939bdc4adca0eea2d4420eef4b/latestFinished?X-DEVOPS-PROJECT-ID=qq-guild-open)
10 |
11 | _✨ 基于 [机器人开放平台API](https://bot.q.qq.com/wiki/develop/api/) 实现的机器人框架 ✨_
12 |
13 | _✨ 为开发者提供一个易使用、开发效率高的开发框架 ✨_
14 |
15 | [文档](https://bot.q.qq.com/wiki/develop/pythonsdk/)
16 | ·
17 | [下载](https://github.com/tencent-connect/botpy/tags)
18 | ·
19 | [安装](https://bot.q.qq.com/wiki/develop/pythonsdk/#sdk-安装)
20 |
21 |
22 |
23 | ## 准备工作
24 |
25 | ### 安装
26 |
27 | ```bash
28 | pip install qq-botpy
29 | ```
30 |
31 | 更新包的话需要添加 `--upgrade` `兼容版本:python3.8+`
32 |
33 | ### 使用
34 |
35 | 需要使用的地方`import botpy`
36 |
37 | ```python
38 | import botpy
39 | ```
40 |
41 | ### 兼容提示
42 |
43 | > 原机器人的老版本`qq-bot`仍然可以使用,但新接口的支持上会逐渐暂停,此次升级不会影响线上使用的机器人
44 |
45 | ## 版本更新说明
46 | ### v1.1.5
47 | 1. 更新鉴权方式。 新版本通过AppID + AppSecret进行鉴权,需要使用者进行适配。AppSecret见[QQ机器人开发设置页](https://q.qq.com/qqbot/#/developer/developer-setting)中的AppSecret字段。具体适配方式见示例 [鉴权配置示例](./examples/config.example.yaml) [鉴权传参接口变更示例](./examples/demo_at_reply.py)
48 | 2. 增加群和好友内发消息能力。可参考[群内发消息示例](./examples/demo_group_reply_text.py) [好友内发消息示例](./examples/demo_c2c_reply_text.py)
49 | 3. 增加群和好友内发送富媒体消息能力,目前支持图片、视频、语音类型。可参考 [群内发富媒体消息示例](./examples/demo_group_reply_file.py) [好友内发富媒体消息示例](./examples/demo_c2c_reply_file.py)
50 |
51 | ## 使用方式
52 |
53 | ### 快速入门
54 |
55 | #### 步骤1
56 |
57 | 通过继承实现`bot.Client`, 实现自己的机器人Client
58 |
59 | #### 步骤2
60 |
61 | 实现机器人相关事件的处理方法,如 `on_at_message_create`, 详细的事件监听列表,请参考 [事件监听.md](./docs/事件监听.md)
62 |
63 | 如下,是定义机器人被@的后自动回复:
64 |
65 | ```python
66 | import botpy
67 | from botpy.message import Message
68 |
69 | class MyClient(botpy.Client):
70 | async def on_at_message_create(self, message: Message):
71 | await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}")
72 | ```
73 |
74 | ``注意:每个事件会下发具体的数据对象,如`message`相关事件是`message.Message`的对象 (部分事件透传了后台数据,暂未实现对象缓存)``
75 |
76 | #### 步骤3
77 |
78 | 设置机器人需要监听的事件通道,并启动`client`
79 |
80 | ```python
81 | import botpy
82 | from botpy.message import Message
83 |
84 | class MyClient(botpy.Client):
85 | async def on_at_message_create(self, message: Message):
86 | await self.api.post_message(channel_id=message.channel_id, content="content")
87 |
88 | intents = botpy.Intents(public_guild_messages=True)
89 | client = MyClient(intents=intents)
90 | client.run(appid="12345", secret="xxxx")
91 | ```
92 |
93 | ### 备注
94 |
95 | 也可以通过预设置的类型,设置需要监听的事件通道
96 |
97 | ```python
98 | import botpy
99 |
100 | intents = botpy.Intents.none()
101 | intents.public_guild_messages=True
102 | ```
103 |
104 | ### 使用API
105 |
106 | 如果要使用`api`方法,可以参考如下方式:
107 |
108 | ```python
109 | import botpy
110 | from botpy.message import Message
111 |
112 | class MyClient(botpy.Client):
113 | async def on_at_message_create(self, message: Message):
114 | await self.api.post_message(channel_id=message.channel_id, content="content")
115 | ```
116 |
117 | ## 示例机器人
118 |
119 | [`examples`](./examples/) 目录下存放示例机器人,具体使用可参考[`Readme.md`](./examples/README.md)
120 |
121 | examples/
122 | .
123 | ├── README.md
124 | ├── config.example.yaml # 示例配置文件(需要修改为config.yaml)
125 | ├── demo_announce.py # 机器人公告API使用示例
126 | ├── demo_api_permission.py # 机器人授权查询API使用示例
127 | ├── demo_at_reply.py # 机器人at被动回复async示例
128 | ├── demo_at_reply_ark.py # 机器人at被动回复ark消息示例
129 | ├── demo_at_reply_embed.py # 机器人at被动回复embed消息示例
130 | ├── demo_at_reply_command.py # 机器人at被动使用Command指令装饰器回复消息示例
131 | ├── demo_at_reply_file_data.py # 机器人at被动回复本地图片消息示例
132 | ├── demo_at_reply_keyboard.py # 机器人at被动回复md带内嵌键盘的示例
133 | ├── demo_at_reply_markdown.py # 机器人at被动回复md消息示例
134 | ├── demo_at_reply_reference.py # 机器人at被动回复消息引用示例
135 | ├── demo_dms_reply.py # 机器人私信被动回复示例
136 | ├── demo_get_reaction_users.py # 机器人获取表情表态成员列表示例
137 | ├── demo_guild_member_event.py # 机器人频道成员变化事件示例
138 | ├── demo_interaction.py # 机器人互动事件示例(未启用)
139 | ├── demo_pins_message.py # 机器人消息置顶示例
140 | ├── demo_recall.py # 机器人消息撤回示例
141 | ├── demo_schedule.py # 机器人日程相关示例
142 |
143 | # 参与开发
144 |
145 | ## 环境配置
146 |
147 | ```bash
148 | pip install -r requirements.txt # 安装依赖的pip包
149 |
150 | pre-commit install # 安装格式化代码的钩子
151 | ```
152 |
153 | ## 单元测试
154 |
155 | 代码库提供API接口测试和 websocket 的单测用例,位于 `tests` 目录中。如果需要自己运行,可以在 `tests` 目录重命名 `.test.yaml` 文件后添加自己的测试参数启动测试:
156 |
157 | ### 单测执行方法
158 |
159 | 先确保已安装 `pytest` :
160 |
161 | ```bash
162 | pip install pytest
163 | ```
164 |
165 | 然后在项目根目录下执行单测:
166 |
167 | ```bash
168 | pytest
169 | ```
170 |
171 | ## 致谢
172 |
173 | 感谢感谢以下开发者对 `botpy` 作出的贡献:
174 |
175 |
176 |
177 |
178 |
179 | # 加入官方社区
180 |
181 | 欢迎扫码加入**QQ 频道开发者社区**。
182 |
183 | 
184 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. role:: raw-html-m2r(raw)
2 | :format: html
3 |
4 |
5 | botpy
6 | =====
7 |
8 | **botpy** 是基于\ `机器人开放平台API `_ 实现的机器人框架,目的提供一个易使用、开发效率高的开发框架。
9 |
10 |
11 | .. image:: https://img.shields.io/pypi/v/qq-botpy
12 | :target: https://img.shields.io/pypi/v/qq-botpy
13 | :alt: PyPI
14 |
15 |
16 | .. image:: https://api.bkdevops.qq.com/process/api/external/pipelines/projects/qq-guild-open/p-713959939bdc4adca0eea2d4420eef4b/badge?X-DEVOPS-PROJECT-ID=qq-guild-open
17 | :target: https://devops.woa.com/process/api-html/user/builds/projects/qq-guild-open/pipelines/p-713959939bdc4adca0eea2d4420eef4b/latestFinished?X-DEVOPS-PROJECT-ID=qq-guild-open
18 | :alt: BK Pipelines Status
19 |
20 |
21 | 准备工作
22 | --------
23 |
24 | 安装
25 | ^^^^
26 |
27 | .. code-block:: bash
28 |
29 | pip install qq-botpy
30 |
31 | 更新包的话需要添加 ``--upgrade`` ``注:需要python3.7+``
32 |
33 | 使用
34 | ^^^^
35 |
36 | 需要使用的地方\ ``import botpy``
37 |
38 | .. code-block:: python
39 |
40 | import botpy
41 |
42 | 兼容提示
43 | ^^^^^^^^
44 |
45 | ..
46 |
47 | 原机器人的老版本\ ``qq-bot``\ 仍然可以使用,但新接口的支持上会逐渐暂停,此次升级不会影响线上使用的机器人
48 |
49 |
50 | 使用方式
51 | --------
52 |
53 | 快速入门
54 | ^^^^^^^^
55 |
56 | 步骤1
57 | ~~~~~
58 |
59 | 通过继承实现\ ``bot.Client``\ , 实现自己的机器人Client
60 |
61 | 步骤2
62 | ~~~~~
63 |
64 | 实现机器人相关事件的处理方法,如 ``on_at_message_create``\ , 详细的事件监听列表,请参考 `事件监听.md <./docs/事件监听.md>`_
65 |
66 | 如下,是定义机器人被@的后自动回复:
67 |
68 | .. code-block:: python
69 |
70 | import botpy
71 | from botpy.types.message import Message
72 |
73 | class MyClient(botpy.Client):
74 | async def on_ready(self):
75 | print(f"robot 「{self.robot.name}」 on_ready!")
76 |
77 | async def on_at_message_create(self, message: Message):
78 | await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}")
79 |
80 | ``注意:每个事件会下发具体的数据对象,如`message`相关事件是`message.Message`的对象 (部分事件透传了后台数据,暂未实现对象缓存)``
81 |
82 | 步骤3
83 | ~~~~~
84 |
85 | 设置机器人需要监听的事件通道,并启动\ ``client``
86 |
87 | .. code-block:: python
88 |
89 | import botpy
90 | from botpy.types.message import Message
91 |
92 | class MyClient(botpy.Client):
93 | async def on_at_message_create(self, message: Message):
94 | await self.api.post_message(channel_id=message.channel_id, content="content")
95 |
96 | intents = botpy.Intents(public_guild_messages=True)
97 | client = MyClient(intents=intents)
98 | client.run(appid="12345", token="xxxx")
99 |
100 | 备注
101 | ^^^^
102 |
103 | 也可以通过预设置的类型,设置需要监听的事件通道
104 |
105 | .. code-block:: python
106 |
107 | import botpy
108 |
109 | intents = botpy.Intents.none()
110 | intents.public_guild_messages=True
111 |
112 | 使用API
113 | ^^^^^^^
114 |
115 | 如果要使用\ ``api``\ 方法,可以参考如下方式:
116 |
117 | .. code-block:: python
118 |
119 | import botpy
120 | from botpy.types.message import Message
121 |
122 | class MyClient(botpy.Client):
123 | async def on_at_message_create(self, message: Message):
124 | await self.api.post_message(channel_id=message.channel_id, content="content")
125 |
126 | 示例机器人
127 | ----------
128 |
129 | `\ ``examples`` <./examples/>`_ 目录下存放示例机器人,具体使用可参考\ `\ ``Readme.md`` <./examples/README.md>`_
130 |
131 | .. code-block::
132 |
133 | examples/
134 | .
135 | ├── README.md
136 | ├── config.example.yaml # 示例配置文件(需要修改为config.yaml)
137 | ├── demo_announce.py # 机器人公告API使用示例
138 | ├── demo_api_permission.py # 机器人授权查询API使用示例
139 | ├── demo_at_reply.py # 机器人at被动回复async示例
140 | ├── demo_at_reply_ark.py # 机器人at被动回复ark消息示例
141 | ├── demo_at_reply_embed.py # 机器人at被动回复embed消息示例
142 | ├── demo_at_reply_command.py # 机器人at被动使用Command指令装饰器回复消息示例
143 | ├── demo_at_reply_file_data.py # 机器人at被动回复本地图片消息示例
144 | ├── demo_at_reply_keyboard.py # 机器人at被动回复md带内嵌键盘的示例
145 | ├── demo_at_reply_markdown.py # 机器人at被动回复md消息示例
146 | ├── demo_at_reply_reference.py # 机器人at被动回复消息引用示例
147 | ├── demo_dms_reply.py # 机器人私信被动回复示例
148 | ├── demo_get_reaction_users.py # 机器人获取表情表态成员列表示例
149 | ├── demo_guild_member_event.py # 机器人频道成员变化事件示例
150 | ├── demo_interaction.py # 机器人互动事件示例(未启用)
151 | ├── demo_pins_message.py # 机器人消息置顶示例
152 | ├── demo_recall.py # 机器人消息撤回示例
153 | ├── demo_schedule.py # 机器人日程相关示例
154 |
155 | 更多功能
156 | --------
157 | 更多功能请参考: [https://github.com/tencent-connect/botpy]
158 |
159 |
--------------------------------------------------------------------------------
/botpy/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from .logging import get_logger
3 | from .client import *
4 | from .flags import *
5 |
6 | logger = get_logger()
7 |
--------------------------------------------------------------------------------
/botpy/audio.py:
--------------------------------------------------------------------------------
1 | from .api import BotAPI
2 | from .types import audio
3 |
4 |
5 | class Audio:
6 | __slots__ = (
7 | "_api",
8 | "_ctx",
9 | "channel_id",
10 | "guild_id",
11 | "audio_url",
12 | "text",
13 | "event_id")
14 |
15 | def __init__(self, api: BotAPI, event_id, data: audio.AudioAction):
16 | self._api = api
17 |
18 | self.channel_id = data.get("channel_id", None)
19 | self.guild_id = data.get("guild_id", None)
20 | self.audio_url = data.get("audio_url", None)
21 | self.text = data.get("text", None)
22 | self.event_id = event_id
23 |
24 | def __repr__(self):
25 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith('_')})
26 |
27 |
28 | class PublicAudio:
29 | __slots__ = (
30 | "_api",
31 | "_ctx",
32 | "guild_id",
33 | "channel_id",
34 | "channel_type",
35 | "user_id")
36 |
37 | def __init__(self, api: BotAPI, data: audio.AudioLive):
38 | self._api = api
39 | self.guild_id = data.get("guild_id", None)
40 | self.channel_id = data.get("channel_id", None)
41 | self.channel_type = data.get("channel_type", None)
42 | self.user_id = data.get("user_id", None)
43 |
44 | def __repr__(self):
45 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith('_')})
46 |
--------------------------------------------------------------------------------
/botpy/channel.py:
--------------------------------------------------------------------------------
1 | from .api import BotAPI
2 | from .types import channel
3 |
4 |
5 | class Channel:
6 | __slots__ = (
7 | "_api",
8 | "guild_id",
9 | "id",
10 | "name",
11 | "type",
12 | "sub_type",
13 | "position",
14 | "owner_id",
15 | "private_type",
16 | "speak_permission",
17 | "application_id",
18 | "permissions",
19 | "event_id",
20 | )
21 |
22 | def __init__(self, api: BotAPI, event_id, data: channel.ChannelPayload):
23 | self._api = api
24 |
25 | self.id = data.get("id", None)
26 | self.name = data.get("name", None)
27 | self.type = data.get("type", None)
28 | self.sub_type = data.get("sub_type", None)
29 | self.position = data.get("position", None)
30 | self.owner_id = data.get("owner_id", None)
31 | self.private_type = data.get("private_type", None)
32 | self.speak_permission = data.get("speak_permission", None)
33 | self.application_id = data.get("application_id", None)
34 | self.permissions = data.get("permissions", None)
35 | self.event_id = event_id
36 |
37 | def __repr__(self):
38 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith('_')})
39 |
--------------------------------------------------------------------------------
/botpy/client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import traceback
3 | from types import TracebackType
4 | from typing import Any, Callable, Coroutine, Dict, List, Tuple, Optional, Union, Type
5 |
6 | from . import logging
7 | from .api import BotAPI
8 | from .connection import ConnectionSession
9 | from .flags import Intents
10 | from .gateway import BotWebSocket
11 | from .http import BotHttp
12 | from .robot import Robot, Token
13 |
14 | _log = logging.get_logger()
15 |
16 |
17 | class _LoopSentinel:
18 | __slots__ = ()
19 |
20 | def __getattr__(self, attr: str) -> None:
21 | raise AttributeError("无法在非异步上下文中访问循环属性")
22 |
23 |
24 | _loop: Any = _LoopSentinel()
25 |
26 |
27 | class Client:
28 | """``Client` 是一个用于与 QQ频道机器人 Websocket 和 API 交互的类。"""
29 |
30 | def __init__(
31 | self,
32 | intents: Intents,
33 | timeout: int = 5,
34 | is_sandbox=False,
35 | log_config: Union[str, dict] = None,
36 | log_format: str = None,
37 | log_level: int = None,
38 | bot_log: Union[bool, None] = True,
39 | ext_handlers: Union[dict, List[dict], bool] = True,
40 | ):
41 | """
42 | Args:
43 | intents (Intents): 通道:机器人需要注册的通道事件code,通过Intents提供的方法获取。
44 | timeout (int): 机器人 HTTP 请求的超时时间。. Defaults to 5
45 | is_sandbox: 是否使用沙盒环境。. Defaults to False
46 |
47 | log_config: 日志配置,可以为dict或.json/.yaml文件路径,会从文件中读取(logging.config.dictConfig)。Default to None(不做更改)
48 | log_format: 控制台输出格式(logging.basicConfig(format=))。Default to None(不做更改)
49 | log_level: 控制台输出level。Default to None(不做更改),
50 | bot_log: bot_log: bot_log: 是否启用bot日志 True/启用 None/禁用拓展 False/禁用拓展+控制台输出
51 | ext_handlers: ext_handlers: 额外的handler,格式参考 logging.DEFAULT_FILE_HANDLER。Default to True(使用默认追加handler)
52 | """
53 | self.intents: int = intents.value
54 | self.ret_coro: bool = False
55 | # TODO loop的整体梳理 @veehou
56 | self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
57 | self.http: BotHttp = BotHttp(timeout=timeout, is_sandbox=is_sandbox)
58 | self.api: BotAPI = BotAPI(http=self.http)
59 |
60 | self._connection: Optional[ConnectionSession] = None
61 | self._closed: bool = False
62 | self._listeners: Dict[str, List[Tuple[asyncio.Future, Callable[..., bool]]]] = {}
63 | self._ws_ap: Dict = {}
64 |
65 | logging.configure_logging(
66 | config=log_config,
67 | _format=log_format,
68 | level=log_level,
69 | bot_log=bot_log,
70 | ext_handlers=ext_handlers,
71 | )
72 |
73 | async def __aenter__(self):
74 | _log.debug("[botpy] 机器人客户端: __aenter__")
75 | await self._async_setup_hook()
76 | return self
77 |
78 | async def __aexit__(
79 | self,
80 | exc_type: Optional[Type[BaseException]],
81 | exc_value: Optional[BaseException],
82 | traceback: Optional[TracebackType],
83 | ) -> None:
84 | _log.debug("[botpy] 机器人客户端: __aexit__")
85 |
86 | if not self.is_closed():
87 | await self.close()
88 |
89 | @property
90 | def robot(self):
91 | return self._connection.state.robot
92 |
93 | async def close(self) -> None:
94 | """关闭client相关的连接"""
95 |
96 | if self._closed:
97 | return
98 |
99 | self._closed = True
100 |
101 | await self.http.close()
102 |
103 | def is_closed(self) -> bool:
104 | return self._closed
105 |
106 | async def on_ready(self):
107 | pass
108 |
109 | async def on_error(self, event_method: str, *args: Any, **kwargs: Any) -> None:
110 | traceback.print_exc()
111 |
112 | async def _async_setup_hook(self) -> None:
113 | # Called whenever the client needs to initialise asyncio objects with a running loop
114 | self.loop = asyncio.get_running_loop()
115 | self._ready = asyncio.Event()
116 |
117 | def run(self, *args: Any, **kwargs: Any) -> None:
118 | """
119 | 机器人服务开始执行
120 |
121 | 注意:
122 | 这个函数必须是最后一个调用的函数,因为它是阻塞的。这意味着事件的注册或在此函数调用之后调用的任何内容在它返回之前不会执行。
123 | 如果想获取协程对象,可以使用`start`方法执行服务, 如:
124 | ```
125 | async with Client as c:
126 | c.start()
127 | ```
128 | """
129 |
130 | async def runner():
131 | async with self:
132 | await self.start(*args, **kwargs)
133 |
134 | try:
135 | self.loop.run_until_complete(runner())
136 | except KeyboardInterrupt:
137 | return
138 |
139 | async def start(self, appid: str, secret: str, ret_coro: bool = False) -> Optional[Coroutine]:
140 | """机器人开始执行
141 |
142 | 参数
143 | ------------
144 | appid: :class:`str`
145 | 机器人 appid
146 | secret: :class:`str`
147 | 机器人 secret
148 | ret_coro: :class:`bool`
149 | 是否需要返回协程对象
150 | """
151 | # login后再进行后面的操作
152 | token = Token(appid, secret)
153 | self.ret_coro = ret_coro
154 |
155 | if self.loop is _loop:
156 | await self._async_setup_hook()
157 |
158 | await self._bot_login(token)
159 | return await self._bot_init(token)
160 |
161 | async def _bot_login(self, token: Token) -> None:
162 | _log.info("[botpy] 登录机器人账号中...")
163 |
164 | user = await self.http.login(token)
165 |
166 | # 通过api获取websocket链接
167 | self._ws_ap = await self.api.get_ws_url()
168 |
169 | # 实例一个session_pool
170 | self._connection = ConnectionSession(
171 | max_async=self._ws_ap["session_start_limit"]["max_concurrency"],
172 | connect=self.bot_connect,
173 | dispatch=self.ws_dispatch,
174 | loop=self.loop,
175 | api=self.api,
176 | )
177 |
178 | self._connection.state.robot = Robot(user)
179 |
180 | async def _bot_init(self, token):
181 | _log.info("[botpy] 程序启动...")
182 | # 每个机器人创建的连接数不能超过remaining剩余连接数
183 | if self._ws_ap["shards"] > self._ws_ap["session_start_limit"]["remaining"]:
184 | raise Exception("[botpy] 超出会话限制...")
185 |
186 | # 根据session限制建立链接
187 | concurrency = self._ws_ap["session_start_limit"]["max_concurrency"]
188 | session_interval = round(5 / concurrency)
189 |
190 | # 根据限制建立分片的并发链接数
191 | _log.debug(f'[botpy] 会话间隔: {session_interval}, 分片: {self._ws_ap["shards"]}, 事件代码: {self.intents}')
192 | return await self._pool_init(token.bot_token(), session_interval)
193 |
194 | async def _pool_init(self, token, session_interval):
195 | def _loop_exception_handler(_loop, context):
196 | # first, handle with default handler
197 | _loop.default_exception_handler(context)
198 |
199 | exception = context.get("exception")
200 | if isinstance(exception, ZeroDivisionError):
201 | _loop.stop()
202 |
203 | for i in range(self._ws_ap["shards"]):
204 | session = {
205 | "session_id": "",
206 | "last_seq": 0,
207 | "intent": self.intents,
208 | "token": token,
209 | "url": self._ws_ap["url"],
210 | "shards": {"shard_id": i, "shard_count": self._ws_ap["shards"]},
211 | }
212 | self._connection.add(session)
213 |
214 | loop = self._connection.loop
215 | loop.set_exception_handler(_loop_exception_handler)
216 |
217 | while not self._closed:
218 | _log.debug("[botpy] 会话循环检查...")
219 | try:
220 | # 返回协程对象,交由开发者自行调控
221 | coroutine = self._connection.multi_run(session_interval)
222 | if self.ret_coro:
223 | return coroutine
224 | elif coroutine:
225 | await coroutine
226 | else:
227 | await self.close()
228 | _log.info("[botpy] 服务意外停止!")
229 | except KeyboardInterrupt:
230 | _log.info("[botpy] 服务强行停止!")
231 | # cancel all tasks lingering
232 |
233 | async def bot_connect(self, session):
234 | """
235 | newConnect 启动一个新的连接,如果连接在监听过程中报错了,或者被远端关闭了链接,需要识别关闭的原因,能否继续 resume
236 | 如果能够 resume,则往 sessionChan 中放入带有 sessionID 的 session
237 | 如果不能,则清理掉 sessionID,将 session 放入 sessionChan 中
238 | session 的启动,交给 start 中的 for 循环执行,session 不自己递归进行重连,避免递归深度过深
239 |
240 | param session: session对象
241 | """
242 | _log.info("[botpy] 会话启动中...")
243 |
244 | client = BotWebSocket(session, self._connection)
245 | try:
246 | await client.ws_connect()
247 | except (Exception, KeyboardInterrupt, SystemExit) as e:
248 | await client.on_error(e)
249 |
250 | def ws_dispatch(self, event: str, *args: Any, **kwargs: Any) -> None:
251 | """分发ws的下行事件
252 |
253 | 解析client类的on_event事件,进行对应的事件回调
254 | """
255 | _log.debug("[botpy] 调度事件: %s", event)
256 | method = "on_" + event
257 |
258 | if hasattr(self, method):
259 | coro = getattr(self, method)
260 | self._schedule_event(coro, method, *args, **kwargs)
261 | else:
262 | _log.debug("[botpy] 事件: %s 未注册", event)
263 |
264 |
265 | def _schedule_event(
266 | self,
267 | coro: Callable[..., Coroutine[Any, Any, Any]],
268 | event_name: str,
269 | *args: Any,
270 | **kwargs: Any,
271 | ) -> asyncio.Task:
272 | wrapped = self._run_event(coro, event_name, *args, **kwargs)
273 | # Schedules the task
274 | return self.loop.create_task(wrapped, name=f"[botpy] {event_name}")
275 |
276 | async def _run_event(
277 | self,
278 | coro: Callable[..., Coroutine[Any, Any, Any]],
279 | event_name: str,
280 | *args: Any,
281 | **kwargs: Any,
282 | ) -> None:
283 | try:
284 | _log.debug("[botpy] _run_event")
285 | await coro(*args, **kwargs)
286 | except asyncio.CancelledError:
287 | pass
288 | except Exception:
289 | try:
290 | await self.on_error(event_name, *args, **kwargs)
291 | except asyncio.CancelledError:
292 | pass
293 |
--------------------------------------------------------------------------------
/botpy/connection.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import inspect
3 | from typing import List, Callable, Dict, Any, Optional
4 |
5 | from .channel import Channel
6 | from .guild import Guild
7 | from .interaction import Interaction
8 | from .manage import C2CManageEvent, GroupManageEvent
9 | from .message import C2CMessage, GroupMessage, Message, DirectMessage, MessageAudit
10 | from .user import Member
11 | from .reaction import Reaction
12 | from .audio import Audio, PublicAudio
13 | from .forum import Thread, OpenThread
14 |
15 | from . import logging
16 | from .api import BotAPI
17 | from .robot import Robot
18 | from .types import session
19 |
20 | _log = logging.get_logger()
21 |
22 |
23 | class ConnectionSession:
24 | """Client的Websocket连接会话
25 |
26 | SessionPool主要支持session的重连,可以根据session的状态动态设置是否需要进行重连操作
27 | 这里通过设置session_id=""空则任务session需要重连
28 | """
29 |
30 | def __init__(
31 | self,
32 | max_async,
33 | connect: Callable,
34 | dispatch: Callable,
35 | loop=None,
36 | api: BotAPI = None,
37 | ):
38 | self.dispatch = dispatch
39 | self.state = ConnectionState(dispatch, api)
40 | self.parser: Dict[str, Callable[[dict], None]] = self.state.parsers
41 |
42 | self._connect = connect
43 | self._max_async = max_async
44 | self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() if loop is None else loop
45 | # session链接同时最大并发数
46 | self._session_list: List[session.Session] = []
47 |
48 | async def multi_run(self, session_interval=5):
49 | if len(self._session_list) == 0:
50 | return
51 | # 根据并发数同时建立多个future
52 | index = 0
53 | session_list = self._session_list
54 | # 需要执行的链接列表,通过time_interval控制启动时间
55 | tasks = []
56 |
57 | while len(session_list) > 0:
58 | _log.debug("[botpy] 会话列表循环运行")
59 | time_interval = session_interval * (index + 1)
60 | _log.info("[botpy] 最大并发连接数: %s, 启动会话数: %s" % (self._max_async, len(session_list)))
61 | for i in range(self._max_async):
62 | if len(session_list) == 0:
63 | break
64 | tasks.append(asyncio.ensure_future(self._runner(session_list.pop(i), time_interval), loop=self.loop))
65 | index += self._max_async
66 |
67 | await asyncio.wait(tasks)
68 |
69 | async def _runner(self, session, time_interval):
70 | await self._connect(session)
71 | # 后台有频率限制,根据间隔时间发起链接请求
72 | await asyncio.sleep(time_interval)
73 |
74 | def add(self, _session: session.Session):
75 | self._session_list.append(_session)
76 |
77 |
78 | class ConnectionState:
79 | """Client的Websocket状态处理"""
80 |
81 | def __init__(self, dispatch: Callable, api: BotAPI):
82 | self.robot: Optional[Robot] = None
83 |
84 | self.parsers: Dict[str, Callable[[Any], None]]
85 | self.parsers = {}
86 | for attr, func in inspect.getmembers(self):
87 | if attr.startswith("parse_"):
88 | self.parsers[attr[6:].lower()] = func
89 |
90 | self._dispatch = dispatch
91 | self.api = api
92 |
93 | def parse_ready(self, payload):
94 | self._dispatch("ready")
95 |
96 | def parse_resumed(self, payload):
97 | self._dispatch("resumed")
98 |
99 | # botpy.flags.Intents.guilds
100 | def parse_guild_create(self, payload):
101 | _guild = Guild(self.api, payload.get('id', None), payload.get('d', {}))
102 | self._dispatch("guild_create", _guild)
103 |
104 | def parse_guild_update(self, payload):
105 | _guild = Guild(self.api, payload.get('id', None), payload.get('d', {}))
106 | self._dispatch("guild_update", _guild)
107 |
108 | def parse_guild_delete(self, payload):
109 | _guild = Guild(self.api, payload.get('id', None), payload.get('d', {}))
110 | self._dispatch("guild_delete", _guild)
111 |
112 | def parse_channel_create(self, payload):
113 | _channel = Channel(self.api, payload.get('id', None), payload.get('d', {}))
114 | self._dispatch("channel_create", _channel)
115 |
116 | def parse_channel_update(self, payload):
117 | _channel = Channel(self.api, payload.get('id', None), payload.get('d', {}))
118 | self._dispatch("channel_update", _channel)
119 |
120 | def parse_channel_delete(self, payload):
121 | _channel = Channel(self.api, payload.get('id', None), payload.get('d', {}))
122 | self._dispatch("channel_delete", _channel)
123 |
124 | # botpy.flags.Intents.guild_members
125 | def parse_guild_member_add(self, payload):
126 | _member = Member(self.api, payload.get('id', None), payload.get('d', {}))
127 | self._dispatch("guild_member_add", _member)
128 |
129 | def parse_guild_member_update(self, payload):
130 | _member = Member(self.api, payload.get('id', None), payload.get('d', {}))
131 | self._dispatch("guild_member_update", _member)
132 |
133 | def parse_guild_member_remove(self, payload):
134 | _member = Member(self.api, payload.get('id', None), payload.get('d', {}))
135 | self._dispatch("guild_member_remove", _member)
136 |
137 | # botpy.flags.Intents.guild_messages
138 | def parse_message_create(self, payload):
139 | _message = Message(self.api, payload.get('id', None), payload.get('d', {}))
140 | self._dispatch("message_create", _message)
141 |
142 | def parse_message_delete(self, payload):
143 | _message = Message(self.api, payload.get('id', None), payload.get('d', {}))
144 | self._dispatch("message_delete", _message)
145 |
146 | # botpy.flags.Intents.guild_message_reactions
147 | def parse_message_reaction_add(self, payload):
148 | _reaction = Reaction(self.api, payload.get('id', None), payload.get('d', {}))
149 | self._dispatch("message_reaction_add", _reaction)
150 |
151 | def parse_message_reaction_remove(self, payload):
152 | _reaction = Reaction(self.api, payload.get('id', None), payload.get('d', {}))
153 | self._dispatch("message_reaction_remove", _reaction)
154 |
155 | # botpy.flags.Intents.direct_message
156 | def parse_direct_message_create(self, payload):
157 | _message = DirectMessage(self.api, payload.get('id', None), payload.get('d', {}))
158 | self._dispatch("direct_message_create", _message)
159 |
160 | def parse_direct_message_delete(self, payload):
161 | _message = DirectMessage(self.api, payload.get('id', None), payload.get('d', {}))
162 | self._dispatch("direct_message_delete", _message)
163 |
164 | # botpy.flags.Intents.interaction
165 | def parse_interaction_create(self, payload):
166 | _interaction = Interaction(self.api, payload.get('id', None), payload.get('d', {}))
167 | self._dispatch("interaction_create", _interaction)
168 |
169 | # botpy.flags.Intents.message_audit
170 | def parse_message_audit_pass(self, payload):
171 | _message_audit = MessageAudit(self.api, payload.get('id', None), payload.get('d', {}))
172 | self._dispatch("message_audit_pass", _message_audit)
173 |
174 | def parse_message_audit_reject(self, payload):
175 | _message_audit = MessageAudit(self.api, payload.get('id', None), payload.get('d', {}))
176 | self._dispatch("message_audit_reject", _message_audit)
177 |
178 | # botpy.flags.Intents.audio_action
179 | def parse_audio_start(self, payload):
180 | _audio = Audio(self.api, payload.get('id', None), payload.get('d', {}))
181 | self._dispatch("audio_start", _audio)
182 |
183 | def parse_audio_finish(self, payload):
184 | _audio = Audio(self.api, payload.get('id', None), payload.get('d', {}))
185 | self._dispatch("audio_finish", _audio)
186 |
187 | def parse_on_mic(self, payload):
188 | _audio = Audio(self.api, payload.get('id', None), payload.get('d', {}))
189 | self._dispatch("on_mic", _audio)
190 |
191 | def parse_off_mic(self, payload):
192 | _audio = Audio(self.api, payload.get('id', None), payload.get('d', {}))
193 | self._dispatch("off_mic", _audio)
194 |
195 | # botpy.flags.Intents.public_guild_messages
196 | def parse_at_message_create(self, payload):
197 | _message = Message(self.api, payload.get('id', None), payload.get('d', {}))
198 | self._dispatch("at_message_create", _message)
199 |
200 | def parse_public_message_delete(self, payload):
201 | _message = Message(self.api, payload.get('id', None), payload.get('d', {}))
202 | self._dispatch("public_message_delete", _message)
203 |
204 | # botpy.flags.Intents.public_messages
205 | def parse_group_at_message_create(self, payload):
206 | _message = GroupMessage(self.api, payload.get("id", None), payload.get("d", {}))
207 | self._dispatch("group_at_message_create", _message)
208 |
209 | def parse_c2c_message_create(self, payload):
210 | _message = C2CMessage(self.api, payload.get("id", None), payload.get("d", {}))
211 | self._dispatch("c2c_message_create", _message)
212 |
213 | def parse_group_add_robot(self, payload):
214 | _event = GroupManageEvent(self.api, payload.get("id", None), payload.get("d", {}))
215 | self._dispatch("group_add_robot", _event)
216 |
217 | def parse_group_del_robot(self, payload):
218 | _event = GroupManageEvent(self.api, payload.get("id", None), payload.get("d", {}))
219 | self._dispatch("group_del_robot", _event)
220 |
221 | def parse_group_msg_reject(self, payload):
222 | _event = GroupManageEvent(self.api, payload.get("id", None), payload.get("d", {}))
223 | self._dispatch("group_msg_reject", _event)
224 |
225 | def parse_group_msg_receive(self, payload):
226 | _event = GroupManageEvent(self.api, payload.get("id", None), payload.get("d", {}))
227 | self._dispatch("group_msg_receive", _event)
228 |
229 | def parse_friend_add(self, payload):
230 | _event = C2CManageEvent(self.api, payload.get("id", None), payload.get("d", {}))
231 | self._dispatch("friend_add", _event)
232 |
233 | def parse_friend_del(self, payload):
234 | _event = C2CManageEvent(self.api, payload.get("id", None), payload.get("d", {}))
235 | self._dispatch("friend_del", _event)
236 |
237 | def parse_c2c_msg_reject(self, payload):
238 | _event = C2CManageEvent(self.api, payload.get("id", None), payload.get("d", {}))
239 | self._dispatch("c2c_msg_reject", _event)
240 |
241 | def parse_c2c_msg_receive(self, payload):
242 | _event = C2CManageEvent(self.api, payload.get("id", None), payload.get("d", {}))
243 | self._dispatch("c2c_msg_receive", _event)
244 |
245 | # botpy.flags.Intents.forums
246 | def parse_forum_thread_create(self, payload):
247 | _forum = Thread(self.api, payload.get('id', None), payload.get('d', {}))
248 | self._dispatch("forum_thread_create", _forum)
249 |
250 | def parse_forum_thread_update(self, payload):
251 | _forum = Thread(self.api, payload.get('id', None), payload.get('d', {}))
252 | self._dispatch("forum_thread_update", _forum)
253 |
254 | def parse_forum_thread_delete(self, payload):
255 | _forum = Thread(self.api, payload.get('id', None), payload.get('d', {}))
256 | self._dispatch("forum_thread_delete", _forum)
257 |
258 | def parse_forum_post_create(self, payload):
259 | self._dispatch("forum_post_create", payload.get('d', {}))
260 |
261 | def parse_forum_post_delete(self, payload):
262 | self._dispatch("forum_post_delete", payload.get('d', {}))
263 |
264 | def parse_forum_reply_create(self, payload):
265 | self._dispatch("forum_reply_create", payload.get('d', {}))
266 |
267 | def parse_forum_reply_delete(self, payload):
268 | self._dispatch("forum_reply_delete", payload.get('d', {}))
269 |
270 | def parse_forum_publish_audit_result(self, payload):
271 | self._dispatch("forum_publish_audit_result", payload.get('d', {}))
272 |
273 | def parse_audio_or_live_channel_member_enter(self, payload):
274 | _public_audio = PublicAudio(self.api, payload.get('d', {}))
275 | self._dispatch("audio_or_live_channel_member_enter", _public_audio)
276 |
277 | def parse_audio_or_live_channel_member_exit(self, payload):
278 | _public_audio = PublicAudio(self.api, payload.get('d', {}))
279 | self._dispatch("audio_or_live_channel_member_exit", _public_audio)
280 |
281 | def parse_open_forum_thread_create(self, payload):
282 | _forum = OpenThread(self.api, payload.get('d', {}))
283 | self._dispatch("open_forum_thread_create", _forum)
284 |
285 | def parse_open_forum_thread_update(self, payload):
286 | _forum = OpenThread(self.api, payload.get('d', {}))
287 | self._dispatch("open_forum_thread_update", _forum)
288 |
289 | def parse_open_forum_thread_delete(self, payload):
290 | _forum = OpenThread(self.api, payload.get('d', {}))
291 | self._dispatch("open_forum_thread_delete", _forum)
292 |
293 | def parse_open_forum_post_create(self, payload):
294 | _forum = OpenThread(self.api, payload.get('d', {}))
295 | self._dispatch("open_forum_post_create", payload.get('d', {}))
296 |
297 | def parse_open_forum_post_delete(self, payload):
298 | _forum = OpenThread(self.api, payload.get('d', {}))
299 | self._dispatch("open_forum_post_delete", payload.get('d', {}))
300 |
301 | def parse_open_forum_reply_create(self, payload):
302 | _forum = OpenThread(self.api, payload.get('d', {}))
303 | self._dispatch("open_forum_reply_create", payload.get('d', {}))
304 |
305 | def parse_open_forum_reply_delete(self, payload):
306 | _forum = OpenThread(self.api, payload.get('d', {}))
307 | self._dispatch("open_forum_reply_delete", payload.get('d', {}))
308 |
--------------------------------------------------------------------------------
/botpy/errors.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 |
4 | class AuthenticationFailedError(RuntimeError):
5 | def __init__(self, msg):
6 | self.msgs = msg
7 |
8 | def __str__(self):
9 | return self.msgs
10 |
11 |
12 | class NotFoundError(RuntimeError):
13 | def __init__(self, msg):
14 | self.msgs = msg
15 |
16 | def __str__(self):
17 | return self.msgs
18 |
19 |
20 | class MethodNotAllowedError(RuntimeError):
21 | def __init__(self, msg):
22 | self.msgs = msg
23 |
24 | def __str__(self):
25 | return self.msgs
26 |
27 |
28 | class SequenceNumberError(RuntimeError):
29 | def __init__(self, msg):
30 | self.msgs = msg
31 |
32 | def __str__(self):
33 | return self.msgs
34 |
35 |
36 | class ServerError(RuntimeError):
37 | def __init__(self, msg):
38 | self.msgs = msg
39 |
40 | def __str__(self):
41 | return self.msgs
42 |
43 |
44 | class ForbiddenError(RuntimeError):
45 | def __init__(self, msg):
46 | self.msgs = msg
47 |
48 | def __str__(self):
49 | return self.msgs
50 |
51 |
52 | HttpErrorDict = {
53 | 401: AuthenticationFailedError,
54 | 404: NotFoundError,
55 | 405: MethodNotAllowedError,
56 | 403: ForbiddenError,
57 | 429: SequenceNumberError,
58 | 500: ServerError,
59 | 504: ServerError,
60 | }
61 |
--------------------------------------------------------------------------------
/botpy/ext/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | 这里放一些可用的工具
4 | 方便开发者调用
5 | """
6 |
--------------------------------------------------------------------------------
/botpy/ext/channel_jump/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | 对子频道转跳进行操作(#name)
4 | 注意:
5 | 1、发送格式要求严格(#name ),自动添加的空格不能删除
6 | 2、无法识别真假转跳
7 | 3、当子频道重名时无法准确识别
8 | 4、当提供子频道转跳字段时请弃用本模块
9 | """
10 |
11 | __all__ = [
12 | "get_channel_jump",
13 | "get_channel_jump_strict",
14 | "escape_channel_jump"
15 | ]
16 |
17 | import re
18 | from typing import List, Dict
19 |
20 | from botpy import BotAPI
21 | from botpy.message import Message
22 |
23 |
24 | def get_channel_jump(text: str = None, message: Message = None) -> List[str]:
25 | """
26 | 识别文本中的子频道转跳(粗略)
27 | :param message: 消息对象
28 | :param text: 文本,为空则message.content
29 | :return: 子频道名称列表(不带#)
30 | """
31 | channel_jump_re = re.compile(r"#(.{1,12}?)(?= )")
32 | return channel_jump_re.findall(message.content if text is None else text)
33 |
34 |
35 | async def get_channel_jump_strict(api: BotAPI, message: Message = None, text: str = None,
36 | guild_id: str = None) -> Dict[str, str]:
37 | """
38 | 识别文本中的子频道转跳(准确)
39 | :param api: BotAPI
40 | :param message: 消息对象
41 | :param text: 文本,为空则message.content
42 | :param guild_id: 频道id,为空则message.guild_id
43 | :return: {子频道名称(不带#):子频道id} (去重)
44 | """
45 | channels = await api.get_channels(guild_id or message.guild_id)
46 | text = message.content if text is None else text
47 | jumps = {}
48 |
49 | for channel in channels:
50 | if "#%s " % channel["name"] in text:
51 | jumps[channel["name"]] = channel["id"]
52 |
53 | return jumps
54 |
55 |
56 | async def escape_channel_jump(api: BotAPI, message: Message = None, text: str = None, guild_id: str = None) -> str:
57 | """
58 | 转义子频道转跳 (#name -> <#id>)
59 | :param api: BotAPI
60 | :param message: 消息对象
61 | :param text: 文本,为空则message.content
62 | :param guild_id: 频道id,为空则message.guild_id
63 | :return: 转义后的文本
64 | """
65 | channels = await api.get_channels(guild_id or message.guild_id)
66 | text = message.content if text is None else text
67 |
68 | for channel in channels:
69 | text = text.replace("#%s " % channel["name"], "<#%s> " % channel["id"])
70 |
71 | return text
72 |
--------------------------------------------------------------------------------
/botpy/ext/cog_apscheduler/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from botpy import logging
4 |
5 | from apscheduler.schedulers.asyncio import AsyncIOScheduler
6 |
7 | _log = logging.get_logger()
8 |
9 | scheduler = AsyncIOScheduler()
10 | scheduler.configure({"apscheduler.timezone": "Asia/Shanghai"})
11 |
12 | scheduler.start()
13 | _log.debug("[加载插件] APScheduler 定时任务")
--------------------------------------------------------------------------------
/botpy/ext/cog_yaml/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import yaml
3 |
4 | from typing import Dict, Any
5 |
6 |
7 | def read(yaml_path) -> Dict[str, Any]:
8 | """
9 | 读取指定目录的yaml文件
10 |
11 | :param yaml_path: 相对当前的yaml文件绝对路径
12 | :return:
13 | """
14 | # 加上 ,encoding='utf-8',处理配置文件中含中文出现乱码的情况。
15 | with open(yaml_path, "r", encoding="utf-8") as f:
16 | return yaml.safe_load(f)
--------------------------------------------------------------------------------
/botpy/ext/command_util.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from functools import wraps
3 | from botpy.message import BaseMessage
4 |
5 |
6 | class Commands:
7 | """
8 | 指令装饰器
9 |
10 | Args:
11 | args (tuple): 字符串元组。
12 | """
13 |
14 | def __init__(self, *args):
15 | self.commands = args
16 |
17 | def __call__(self, func):
18 | @wraps(func)
19 | async def decorated(*args, **kwargs):
20 | message: BaseMessage = kwargs["message"]
21 | for command in self.commands:
22 | if command in message.content:
23 | # 分割指令后面的指令参数
24 | params = message.content.split(command)[1].strip()
25 | kwargs["params"] = params
26 | return await func(*args, **kwargs)
27 | return False
28 |
29 | return decorated
30 |
31 |
--------------------------------------------------------------------------------
/botpy/ext/convert_color/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import Union
3 |
4 |
5 | def start(color: Union[tuple, str]) -> int:
6 | """
7 | 将 RGB 值的元组或 HEX 值的字符串转换为单个整数
8 |
9 | Args:
10 | color (tuple or str): 输入RGB的三位tuple或HEX的sting颜色
11 |
12 | Returns:
13 | 颜色的 RGB 值。
14 | """
15 | colors = []
16 | if isinstance(color, tuple):
17 | if len(color) == 3:
18 | for items in color:
19 | if not isinstance(items, int) or items not in range(256):
20 | raise TypeError("RGB颜色应为一个三位数的tuple,且当中每个数值都应该介乎于0和255之间,如(255,255,255)")
21 | colors.append(items)
22 | else:
23 | raise TypeError("RGB颜色应为一个三位数的tuple,且当中每个数值都应该介乎于0和255之间,如(255,255,255)")
24 | elif isinstance(color, str):
25 | colour = color.replace("#", "")
26 | if len(colour) == 6:
27 | for items in (colour[:2], colour[2:4], colour[4:]):
28 | try:
29 | items = int(items, 16)
30 | except ValueError:
31 | raise TypeError("该HEX颜色不存在,请检查其颜色值是否准确")
32 | if items not in range(256):
33 | raise TypeError("该HEX颜色不存在,请检查其颜色值是否准确")
34 | colors.append(items)
35 | else:
36 | raise TypeError('HEX颜色应为一个 #加六位数字或字母 的string,如"#ffffff"')
37 | else:
38 | raise TypeError('颜色值应为RGB的三位tuple,如(255,255,255);或HEX的sting颜色,如"#ffffff"')
39 | return colors[0] + 256 * colors[1] + (256**2) * colors[2]
40 |
--------------------------------------------------------------------------------
/botpy/flags.py:
--------------------------------------------------------------------------------
1 | from typing import Callable, overload, Optional, Any, Type, ClassVar, Dict, Iterator, Tuple, TypeVar
2 |
3 | __all__ = ("Intents", "Permission")
4 |
5 | BF = TypeVar("BF", bound="BaseFlags")
6 |
7 |
8 | def fill_with_flags(*, inverted: bool = False) -> Callable[[Type[BF]], Type[BF]]:
9 | def decorator(cls: Type[BF]) -> Type[BF]:
10 | # fmt: off
11 | cls.VALID_FLAGS = {
12 | name: value.flag
13 | for name, value in cls.__dict__.items()
14 | if isinstance(value, Flag)
15 | }
16 | # fmt: on
17 |
18 | if inverted:
19 | max_bits = max(cls.VALID_FLAGS.values()).bit_length()
20 | cls.DEFAULT_VALUE = -1 + (2**max_bits)
21 | else:
22 | cls.DEFAULT_VALUE = 0
23 |
24 | return cls
25 |
26 | return decorator
27 |
28 |
29 | class BaseFlags:
30 | VALID_FLAGS: ClassVar[Dict[str, int]]
31 | DEFAULT_VALUE: ClassVar[int]
32 |
33 | value: int
34 |
35 | __slots__ = ("value",)
36 |
37 | def __init__(self, **kwargs: bool):
38 | self.value = self.DEFAULT_VALUE
39 | for key, value in kwargs.items():
40 | if key not in self.VALID_FLAGS:
41 | raise TypeError(f"{key!r} is not a valid flag name.")
42 | setattr(self, key, value)
43 |
44 | @classmethod
45 | def _from_value(cls, value):
46 | self = cls.__new__(cls)
47 | self.value = value
48 | return self
49 |
50 | def __eq__(self, other: object) -> bool:
51 | return isinstance(other, self.__class__) and self.value == other.value
52 |
53 | def __ne__(self, other: object) -> bool:
54 | return not self.__eq__(other)
55 |
56 | def __hash__(self) -> int:
57 | return hash(self.value)
58 |
59 | def __repr__(self) -> str:
60 | return f"<{self.__class__.__name__} value={self.value}>"
61 |
62 | def __iter__(self) -> Iterator[Tuple[str, bool]]:
63 | for name, value in self.__class__.__dict__.items():
64 | if isinstance(value, Flag):
65 | yield name, self.has_flag(value.flag)
66 |
67 | def has_flag(self, o: int) -> bool:
68 | return (self.value & o) == o
69 |
70 | def set_flag(self, o: int, toggle: bool) -> None:
71 | if toggle is True:
72 | self.value |= o
73 | elif toggle is False:
74 | self.value &= ~o
75 | else:
76 | raise TypeError(f"Value to set for {self.__class__.__name__} must be a bool.")
77 |
78 |
79 | class Flag:
80 | def __init__(self, func: Callable[[Any], int]):
81 | self.flag: int = func(None)
82 | self.__doc__: Optional[str] = func.__doc__
83 |
84 | @overload
85 | def __get__(self, instance: None, owner: Type[BF]) -> Any:
86 | ...
87 |
88 | @overload
89 | def __get__(self, instance: BF, owner: Type[BF]) -> bool:
90 | ...
91 |
92 | def __get__(self, instance: Optional[BF], owner: Type[BF]) -> Any:
93 | if instance is None:
94 | return self
95 | return instance.has_flag(self.flag)
96 |
97 | def __set__(self, instance: BF, value: bool) -> None:
98 | instance.set_flag(self.flag, value)
99 |
100 | def __repr__(self) -> str:
101 | return f""
102 |
103 |
104 | @fill_with_flags()
105 | class Intents(BaseFlags):
106 | """
107 | public_messages 群/C2C公域消息事件
108 | public_guild_messages 公域消息事件
109 | guild_messages 消息事件 (仅 私域 机器人能够设置此 intents)
110 | direct_message 私信事件
111 | guild_message_reactions 消息相关互动事件
112 | guilds 频道事件
113 | guild_members 频道成员事件
114 | interaction 互动事件
115 | message_audit 消息审核事件
116 | forums 论坛事件 (仅 私域 机器人能够设置此 intents)
117 | audio_action 音频事件
118 | """
119 |
120 | __slots__ = ()
121 |
122 | def __init__(self, **kwargs: bool) -> None:
123 | super().__init__(**kwargs)
124 | self.value: int = self.DEFAULT_VALUE
125 | for key, value in kwargs.items():
126 | if key not in self.VALID_FLAGS:
127 | raise TypeError(f"{key!r} is not a valid flag name.")
128 | setattr(self, key, value)
129 |
130 | @classmethod
131 | def all(cls):
132 | """打开所有事件的监听"""
133 | self = cls.none()
134 | self.guild_messages = True
135 | self.forums = True
136 | self.interaction = True
137 | self.audio_action = True
138 | self.guilds = True
139 | self.guild_members = True
140 | self.guild_message_reactions = True
141 | self.direct_message = True
142 | self.message_audit = True
143 | self.public_messages = True
144 | self.public_guild_messages = True
145 | self.audio_or_live_channel_member = True
146 | self.open_forum_event = True
147 | return self
148 |
149 | @classmethod
150 | def none(cls):
151 | """不主动打开"""
152 | self = cls.__new__(cls)
153 | self.value = self.DEFAULT_VALUE
154 | return self
155 |
156 | @classmethod
157 | def default(cls):
158 | """打开所有公域事件的监听
159 |
160 | `guild_messages` 和 `forums` 需要私域权限
161 | """
162 | self = cls.all()
163 | self.guild_messages = False
164 | self.forums = False
165 | return self
166 |
167 | @Flag
168 | def guilds(self):
169 | """:class:`bool`: 是否打开频道事件的监听.
170 |
171 | 通过增加`client`的`on_xx`成员方法可以获取事件下发的数据:
172 | `py
173 | from botpy.guild import Guild
174 |
175 | async def on_guild_create(self, guild: Guild):
176 | `执行相关的任务`
177 | `
178 |
179 | - :func:`on_guild_create(self, guild: Guild)`: 当机器人加入新guild时
180 | - :func:`on_guild_update(self, guild: Guild)`: 当guild资料发生变更时
181 | - :func:`on_guild_delete(self, guild: Guild)`: 当机器人退出guild时
182 | - :func:`on_channel_create(self, channel: Channel)`: 当channel被创建时
183 | - :func:`on_channel_update(self, channel: Channel)`: 当channel被更新时
184 | - :func:`on_channel_delete(self, channel: Channel)`: 当channel被删除时
185 |
186 | """
187 | return 1 << 0
188 |
189 | @Flag
190 | def guild_members(self):
191 | """:class:`bool`: 是否打开频道成员事件的监听.
192 |
193 | - :func:`on_guild_member_add(self, member:Member)`: 当成员加入时
194 | - :func:`on_guild_member_update(self, member:Member)`: 当成员资料变更时
195 | - :func:`on_guild_member_remove(self, member:Member)`: 当成员被移除时
196 |
197 | """
198 | return 1 << 1
199 |
200 | @Flag
201 | def guild_messages(self):
202 | """:class:`bool`: 是否打开消息事件的监听.
203 |
204 | - :func:`on_message_create(self,message:Message)`:
205 | 发送消息事件,代表频道内的全部消息,而不只是 at 机器人的消息。内容与 AT_MESSAGE_CREATE 相同
206 | - :func:`on_message_delete(self,message:Message)`: 删除(撤回)消息事件
207 |
208 | 注意:仅 *私域* 机器人能够设置此 intents
209 | """
210 | return 1 << 9
211 |
212 | @Flag
213 | def guild_message_reactions(self):
214 | """:class:`bool`: 是否打开消息相关互动事件的监听.
215 |
216 | - :func:`on_message_reaction_add`: 为消息添加表情表态
217 | - :func:`on_message_reaction_remove`: 为消息删除表情表态
218 |
219 | """
220 | return 1 << 10
221 |
222 | @Flag
223 | def direct_message(self):
224 | """:class:`bool`: 是否打开私信事件的监听.
225 |
226 | - :func:`on_direct_message_create`: 当收到用户发给机器人的私信消息时
227 | - :func:`on_direct_message_delete `: 删除(撤回)消息事件
228 |
229 | """
230 | return 1 << 12
231 |
232 | @Flag
233 | def interaction(self):
234 | """:class:`bool`: 是否打开互动事件的监听.
235 |
236 | - :func:`on_interaction_create`: 互动事件创建时
237 |
238 | """
239 | return 1 << 26
240 |
241 | @Flag
242 | def message_audit(self):
243 | """:class:`bool`: 是否打开消息审核事件的监听.
244 |
245 | - :func:`on_message_audit_pass`: 消息审核通过
246 | - :func:`on_message_audit_reject`: 消息审核不通过
247 |
248 | """
249 | return 1 << 27
250 |
251 | @Flag
252 | def forums(self):
253 | """:class:`bool`: 是否打开论坛事件的监听.
254 |
255 | - :func:`on_forum_thread_create` 当用户创建主题时
256 | - :func:`on_forum_thread_update` 当用户更新主题时
257 | - :func:`on_forum_thread_delete` 当用户删除主题时
258 | - :func:`on_forum_post_create` 当用户创建帖子时
259 | - :func:`on_forum_post_delete` 当用户删除帖子时
260 | - :func:`on_forum_reply_create` 当用户回复评论时
261 | - :func:`on_forum_reply_delete` 当用户删除评论时
262 | - :func:`on_forum_publish_audit_result` 当用户发表审核通过时
263 | 注意:仅 *私域* 机器人能够设置此 intents
264 | """
265 | return 1 << 28
266 |
267 | @Flag
268 | def audio_action(self):
269 | """:class:`bool`: 是否打开音频事件的监听.
270 |
271 | - :func:`on_audio_start`: 音频开始播放时
272 | - :func:`on_audio_finish`: 音频播放结束时
273 | - :func:`on_audio_on_mic`: 上麦时
274 | - :func:`on_audio_off_mic`: 下麦时
275 |
276 | """
277 | return 1 << 29
278 |
279 | @Flag
280 | def public_guild_messages(self):
281 | """:class:`bool`: 是否打开公域消息事件的监听.
282 |
283 | 通过增加`client`的`on_xx`事件可以获取事件下发的数据:
284 |
285 | - :func:`on_at_message_create` // 当收到@机器人的消息时
286 | - :func:`on_public_message_delete` // 当频道的消息被删除时
287 |
288 | """
289 | return 1 << 30
290 |
291 | @Flag
292 | def audio_or_live_channel_member(self):
293 | """:class:`bool`: 是否打开音视频/直播子频道成员进出事件的监听.
294 |
295 | 通过增加`client`的`on_xx`事件可以获取事件下发的数据:
296 |
297 | - :func:`on_audio_or_live_channel_enter` // 用户进入音视频/直播子频道时
298 | - :func:`on_audio_or_live_channel_exit` // 用户退出音视频/直播子频道时
299 |
300 | """
301 | return 1 << 19
302 |
303 | @Flag
304 | def open_forum_event(self):
305 | """:class:`bool`: 开放论坛对象事件的监听.
306 |
307 | 通过增加`client`的`on_xx`事件可以获取事件下发的数据:
308 |
309 |
310 | - :func:`on_open_forum_thread_create` // 用户创建主题时
311 | - :func:`on_open_forum_thread_update` // 用户修改主题时
312 | - :func:`on_open_forum_thread_delete` // 用户删除主题时
313 | - :func:`on_open_forum_post_create` // 用户创建帖子时
314 | - :func:`on_open_forum_post_delete` // 用户删除帖子时
315 | - :func:`on_open_forum_reply_create` // 用户回复评论时
316 | - :func:`on_open_forum_reply_delete` // 用户删除评论时
317 |
318 | """
319 | return 1 << 18
320 |
321 | @Flag
322 | def public_messages(self):
323 | """:class:`bool`: 是否打开公域群/C2C消息事件的监听.
324 |
325 | 通过增加`client`的`on_xx`事件可以获取事件下发的数据:
326 |
327 | - :func:`on_group_at_message_create` // 当收到群@机器人的消息时
328 | - :func:`on_c2c_message_create` // 当收到c2c的消息时
329 | - :func:`on_group_add_robot` // 机器人加入群聊
330 | - :func:`on_group_del_robot` // 机器人退出群聊
331 | - :func:`on_group_msg_reject` // 群聊拒绝机器人主动消息
332 | - :func:`on_group_msg_receive` // 群聊接受机器人主动消息
333 | - :func:`on_friend_add` // 用户添加机器人
334 | - :func:`on_friend_del` // 用户删除机器人
335 | - :func:`on_c2c_msg_reject` // 用户拒绝机器人主动消息
336 | - :func:`on_c2c_msg_receive` // 用户接受机器人主动消息
337 |
338 | """
339 | return 1 << 25
340 |
341 |
342 | @fill_with_flags()
343 | class Permission(BaseFlags):
344 | def __init__(self, **kwargs: bool) -> None:
345 | super().__init__(**kwargs)
346 | self.value: int = self.DEFAULT_VALUE
347 | for key, value in kwargs.items():
348 | if key not in self.VALID_FLAGS:
349 | raise TypeError(f"{key!r} is not a valid flag name.")
350 | setattr(self, key, value)
351 |
352 | @Flag
353 | def view_permission(self):
354 | """可查看子频道 0x0000000001 (1 << 0) 支持指定成员可见类型,支持身份组可见类型"""
355 | return 1 << 0
356 |
357 | @Flag
358 | def manager_permission(self):
359 | """可管理子频道 0x0000000002 (1 << 1) 创建者、管理员、子频道管理员都具有此权限"""
360 | return 1 << 1
361 |
362 | @Flag
363 | def speak_permission(self):
364 | """可发言子频道 0x0000000004 (1 << 2) 支持指定成员发言类型,支持身份组发言类型"""
365 | return 1 << 2
366 |
367 | @Flag
368 | def live_permission(self):
369 | """可直播子频道 0x0000000008 (1 << 3) 支持指定成员发起直播,支持身份组发起直播;仅可在直播子频道中设置"""
370 | return 1 << 3
371 |
--------------------------------------------------------------------------------
/botpy/forum.py:
--------------------------------------------------------------------------------
1 | from json import loads
2 |
3 | from .api import BotAPI
4 | from .types import forum
5 |
6 |
7 | class _Text:
8 | def __init__(self, data):
9 | self.text = data.get("text", None)
10 |
11 | def __repr__(self):
12 | return str(self.__dict__)
13 |
14 |
15 | class _Image:
16 | def __init__(self, data):
17 | self.plat_image = self._PlatImage(data.get("plat_image", {}))
18 |
19 | def __repr__(self):
20 | return str(self.__dict__)
21 |
22 | class _PlatImage:
23 | def __init__(self, data):
24 | self.url = data.get("url", None)
25 | self.width = data.get("width", None)
26 | self.height = data.get("height", None)
27 | self.image_id = data.get("image_id", None)
28 |
29 | def __repr__(self):
30 | return str(self.__dict__)
31 |
32 |
33 | class _Video:
34 | def __init__(self, data):
35 | self.plat_video = self._PlatVideo(data.get("plat_video", {}))
36 |
37 | def __repr__(self):
38 | return str(self.__dict__)
39 |
40 | class _PlatVideo:
41 | def __init__(self, data):
42 | self.url = data.get("url", None)
43 | self.width = data.get("width", None)
44 | self.height = data.get("height", None)
45 | self.video_id = data.get("video_id", None)
46 | self.cover = data.get("cover", {})
47 |
48 | def __repr__(self):
49 | return str(self.__dict__)
50 |
51 | class _Cover:
52 | def __init__(self, data):
53 | self.url = data.get("url", None)
54 | self.width = data.get("width", None)
55 | self.height = data.get("height", None)
56 |
57 | def __repr__(self):
58 | return str(self.__dict__)
59 |
60 |
61 | class _Url:
62 | def __init__(self, data):
63 | self.url = data.get("url", None)
64 | self.desc = data.get("desc", None)
65 |
66 | def __repr__(self):
67 | return str(self.__dict__)
68 |
69 |
70 | class Thread:
71 | __slots__ = (
72 | "_api",
73 | "thread_info",
74 | "channel_id",
75 | "guild_id",
76 | "author_id",
77 | "event_id")
78 |
79 | def __init__(self, api: BotAPI, event_id, data: forum.Thread):
80 | self._api = api
81 |
82 | self.author_id = data.get("author_id", None)
83 | self.channel_id = data.get("channel_id", None)
84 | self.guild_id = data.get("guild_id", None)
85 | self.thread_info = self._ThreadInfo(data.get("thread_info", {}))
86 | self.event_id = event_id
87 |
88 | def __repr__(self):
89 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith('_')})
90 |
91 | class _ThreadInfo:
92 | def __init__(self, data):
93 | self.title = self._Title(loads(data.get("title", {})))
94 | self.content = self._Content(loads(data.get("content", {})))
95 | self.thread_id = data.get("thread_id", None)
96 | self.date_time = data.get("date_time", None)
97 |
98 | def __repr__(self):
99 | return str(self.__dict__)
100 |
101 | class _Title:
102 | def __init__(self, data):
103 | self.paragraphs = [self._Paragraphs(items) for items in data.get("paragraphs", {})]
104 |
105 | def __repr__(self):
106 | return str(self.__dict__)
107 |
108 | class _Paragraphs:
109 | def __init__(self, data):
110 | self.elems = [self._Elems(items) for items in data.get("elems", {})]
111 | self.props = data.get("props", None)
112 |
113 | def __repr__(self):
114 | return str(self.__dict__)
115 |
116 | class _Elems:
117 | def __init__(self, data):
118 | self.type = data.get("type", None)
119 | self.text = _Text(data.get("text", {}))
120 |
121 | def __repr__(self):
122 | return str(self.__dict__)
123 |
124 | class _Content:
125 | def __init__(self, data):
126 | self.paragraphs = [self._Paragraphs(items) for items in data.get("paragraphs", {})]
127 |
128 | def __repr__(self):
129 | return str(self.__dict__)
130 |
131 | class _Paragraphs:
132 | def __init__(self, data):
133 | self.elems = [self._Elems(items) for items in data.get("elems", {})]
134 | self.props = data.get("props", None)
135 |
136 | def __repr__(self):
137 | return str(self.__dict__)
138 |
139 | class _Elems:
140 | def __init__(self, data):
141 | self.type = data.get("type", None)
142 | if self.type == 1:
143 | self.text = _Text(data.get("text", {}))
144 | elif self.type == 2:
145 | self.image = _Image(data.get("image", {}))
146 | elif self.type == 3:
147 | self.video = _Video(data.get("video", {}))
148 | elif self.type == 4:
149 | self.url = _Url(data.get("url", {}))
150 |
151 | def __repr__(self):
152 | return str(self.__dict__)
153 |
154 |
155 | class OpenThread:
156 | __slots__ = (
157 | "_api",
158 | "thread_info",
159 | "channel_id",
160 | "guild_id",
161 | "author_id",
162 | "event_id")
163 |
164 | def __init__(self, api: BotAPI, data: forum.OpenForumEvent):
165 | self._api = api
166 |
167 | self.guild_id = data.get("guild_id", None)
168 | self.channel_id = data.get("channel_id", None)
169 | self.author_id = data.get("author_id", None)
170 |
171 | def __repr__(self):
172 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith('_')})
173 |
--------------------------------------------------------------------------------
/botpy/gateway.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import asyncio
3 | import json
4 | import traceback
5 | from typing import Optional
6 |
7 | from aiohttp import WSMessage, ClientWebSocketResponse, TCPConnector, ClientSession, WSMsgType
8 | from ssl import SSLContext
9 |
10 | from . import logging
11 | from .connection import ConnectionSession
12 | from .types import gateway
13 | from .types.session import Session
14 |
15 | _log = logging.get_logger()
16 |
17 |
18 | class BotWebSocket:
19 | """Bot的Websocket实现
20 |
21 | CODE 名称 客户端操作 描述
22 | 0 Dispatch Receive 服务端进行消息推送
23 | 1 Heartbeat Send/Receive 客户端或服务端发送心跳
24 | 2 Identify Send 客户端发送鉴权
25 | 6 Resume Send 客户端恢复连接
26 | 7 Reconnect Receive 服务端通知客户端重新连接
27 | 9 Invalid Session Receive 当identify或resume的时候,如果参数有错,服务端会返回该消息
28 | 10 Hello Receive 当客户端与网关建立ws连接之后,网关下发的第一条消息
29 | 11 Heartbeat ACK Receive 当发送心跳成功之后,就会收到该消息
30 | """
31 |
32 | WS_DISPATCH_EVENT = 0
33 | WS_HEARTBEAT = 1
34 | WS_IDENTITY = 2
35 | WS_RESUME = 6
36 | WS_RECONNECT = 7
37 | WS_INVALID_SESSION = 9
38 | WS_HELLO = 10
39 | WS_HEARTBEAT_ACK = 11
40 |
41 | def __init__(self, session: Session, _connection: ConnectionSession):
42 | self._conn: Optional[ClientWebSocketResponse] = None
43 | self._session = session
44 | self._connection = _connection
45 | self._parser = _connection.parser
46 | self._can_reconnect = True
47 | self._INVALID_RECONNECT_CODE = [9001, 9005]
48 | self._AUTH_FAIL_CODE = [4004]
49 |
50 | async def on_error(self, exception: BaseException):
51 | _log.error("[botpy] websocket连接: %s, 异常信息 : %s" % (self._conn, exception))
52 | traceback.print_exc()
53 | self._connection.add(self._session)
54 |
55 | async def on_closed(self, close_status_code, close_msg):
56 | _log.info("[botpy] 关闭, 返回码: %s" % close_status_code + ", 返回信息: %s" % close_msg)
57 | if close_status_code in self._AUTH_FAIL_CODE:
58 | _log.info("[botpy] 鉴权失败,重置token...")
59 | self._session["token"].access_token = None
60 | # 这种不能重新链接
61 | if close_status_code in self._INVALID_RECONNECT_CODE or not self._can_reconnect:
62 | _log.info("[botpy] 无法重连,创建新连接!")
63 | self._session["session_id"] = ""
64 | self._session["last_seq"] = 0
65 | # 断连后启动一个新的链接并透传当前的session,不使用内部重连的方式,避免死循环
66 | self._connection.add(self._session)
67 |
68 | async def on_message(self, ws, message):
69 | _log.debug("[botpy] 接收消息: %s" % message)
70 | msg = json.loads(message)
71 |
72 | if await self._is_system_event(msg, ws):
73 | return
74 |
75 | event = msg.get("t")
76 | opcode = msg.get("op")
77 | event_seq = msg["s"]
78 | if event_seq > 0:
79 | self._session["last_seq"] = event_seq
80 |
81 | if event == "READY":
82 | # 心跳检查
83 | self._connection.loop.create_task(self._send_heart(interval=30))
84 | ready = await self._ready_handler(msg)
85 | _log.info(f"[botpy] 机器人「{ready['user']['username']}」启动成功!")
86 |
87 | if event == "RESUMED":
88 | # 心跳检查
89 | self._connection.loop.create_task(self._send_heart(interval=30))
90 | _log.info("[botpy] 机器人重连成功! ")
91 |
92 | if event and opcode == self.WS_DISPATCH_EVENT:
93 | event = msg["t"].lower()
94 | try:
95 | func = self._parser[event]
96 | except KeyError:
97 | _log.error("_parser unknown event %s.", event)
98 | else:
99 | func(msg)
100 |
101 | async def on_connected(self, ws: ClientWebSocketResponse):
102 | self._conn = ws
103 | if self._conn is None:
104 | raise Exception("[botpy] websocket连接失败")
105 | if self._session["session_id"]:
106 | await self.ws_resume()
107 | else:
108 | await self.ws_identify()
109 |
110 | async def ws_connect(self):
111 | """
112 | websocket向服务器端发起链接,并定时发送心跳
113 | """
114 |
115 | _log.info("[botpy] 启动中...")
116 | ws_url = self._session["url"]
117 | if not ws_url:
118 | raise Exception("[botpy] 会话url为空")
119 |
120 | # adding SSLContext-containing connector to prevent SSL certificate verify failed error
121 | async with ClientSession(connector=TCPConnector(limit=10, ssl=SSLContext())) as session:
122 | async with session.ws_connect(self._session["url"]) as ws_conn:
123 | while True:
124 | msg: WSMessage
125 | msg = await ws_conn.receive()
126 | if msg.type == WSMsgType.TEXT:
127 | await self.on_message(ws_conn, msg.data)
128 | elif msg.type == WSMsgType.ERROR:
129 | await self.on_error(ws_conn.exception())
130 | await ws_conn.close()
131 | elif msg.type == WSMsgType.CLOSED or msg.type == WSMsgType.CLOSE:
132 | await self.on_closed(ws_conn.close_code, msg.extra)
133 | if ws_conn.closed:
134 | _log.info("[botpy] ws关闭, 停止接收消息!")
135 | break
136 |
137 | async def ws_identify(self):
138 | """websocket鉴权"""
139 | if not self._session["intent"]:
140 | self._session["intent"] = 1
141 |
142 | _log.info("[botpy] 鉴权中...")
143 | await self._session["token"].check_token()
144 | payload = {
145 | "op": self.WS_IDENTITY,
146 | "d": {
147 | "shard": [
148 | self._session["shards"]["shard_id"],
149 | self._session["shards"]["shard_count"],
150 | ],
151 | "token": self._session["token"].get_string(),
152 | "intents": self._session["intent"],
153 | },
154 | }
155 |
156 | await self.send_msg(json.dumps(payload))
157 |
158 | async def send_msg(self, event_json):
159 | """
160 | websocket发送消息
161 | :param event_json:
162 | """
163 | send_msg = event_json
164 | _log.debug("[botpy] 发送消息: %s" % send_msg)
165 | if isinstance(self._conn, ClientWebSocketResponse):
166 | if self._conn.closed:
167 | _log.debug("[botpy] ws连接已关闭! ws对象: %s" % self._conn)
168 | else:
169 | await self._conn.send_str(data=send_msg)
170 |
171 | async def ws_resume(self):
172 | """
173 | websocket重连
174 | """
175 | _log.info("[botpy] 重连启动...")
176 | await self._session["token"].check_token()
177 | payload = {
178 | "op": self.WS_RESUME,
179 | "d": {
180 | "token": self._session["token"].get_string(),
181 | "session_id": self._session["session_id"],
182 | "seq": self._session["last_seq"],
183 | },
184 | }
185 |
186 | await self.send_msg(json.dumps(payload))
187 |
188 | async def _ready_handler(self, message_event) -> gateway.ReadyEvent:
189 | data = message_event["d"]
190 | self.version = data["version"]
191 | self._session["session_id"] = data["session_id"]
192 | self._session["shards"]["shard_id"] = data["shard"][0]
193 | self._session["shards"]["shard_count"] = data["shard"][1]
194 | self.user = data["user"]
195 | return data
196 |
197 | async def _is_system_event(self, message_event, ws):
198 | """
199 | 系统事件
200 | :param message_event:消息
201 | :param ws:websocket
202 | :return:
203 | """
204 | event_op = message_event["op"]
205 | if event_op == self.WS_HELLO:
206 | await self.on_connected(ws)
207 | return True
208 | if event_op == self.WS_HEARTBEAT_ACK:
209 | return True
210 | if event_op == self.WS_RECONNECT:
211 | self._can_reconnect = True
212 | return True
213 | if event_op == self.WS_INVALID_SESSION:
214 | self._can_reconnect = False
215 | return True
216 | return False
217 |
218 | async def _send_heart(self, interval):
219 | """
220 | 心跳包
221 | :param interval: 间隔时间
222 | """
223 | _log.info("[botpy] 心跳维持启动...")
224 | while True:
225 | payload = {
226 | "op": self.WS_HEARTBEAT,
227 | "d": self._session["last_seq"],
228 | }
229 |
230 | if self._conn is None:
231 | _log.debug("[botpy] 连接已关闭!")
232 | return
233 | if self._conn.closed:
234 | _log.debug("[botpy] ws连接已关闭, 心跳检测停止,ws对象: %s" % self._conn)
235 | return
236 |
237 | await self.send_msg(json.dumps(payload))
238 | await asyncio.sleep(interval)
239 |
--------------------------------------------------------------------------------
/botpy/guild.py:
--------------------------------------------------------------------------------
1 | from .api import BotAPI
2 | from .types import guild
3 |
4 |
5 | class Guild:
6 | __slots__ = (
7 | "_api",
8 | "_ctx",
9 | "id",
10 | "name",
11 | "icon",
12 | "owner_id",
13 | "is_owner",
14 | "member_count",
15 | "max_members",
16 | "description",
17 | "joined_at",
18 | "event_id"
19 | )
20 |
21 | def __init__(self, api: BotAPI, event_id, data: guild.GuildPayload):
22 | self._api = api
23 |
24 | self.id = data.get("id", None)
25 | self.name = data.get("name", None)
26 | self.icon = data.get("icon", None)
27 | self.owner_id = data.get("owner_id", None)
28 | self.is_owner = data.get("owner", None)
29 | self.member_count = data.get("member_count", None)
30 | self.max_members = data.get("max_members", None)
31 | self.description = data.get("description", None)
32 | self.joined_at = data.get("joined_at", None)
33 | self.event_id = event_id
34 |
35 | def __repr__(self):
36 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith('_')})
37 |
--------------------------------------------------------------------------------
/botpy/http.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import asyncio
3 | from json.decoder import JSONDecodeError
4 | from ssl import SSLContext
5 | from typing import Any, Optional, ClassVar, Union, Dict
6 |
7 | import aiohttp
8 | from aiohttp import ClientResponse, FormData, TCPConnector, multipart, hdrs, payload
9 |
10 | from . import logging
11 | from .errors import HttpErrorDict, ServerError
12 | from .robot import Token
13 | from .types import robot
14 |
15 | X_TPS_TRACE_ID = "X-Tps-trace-Id"
16 |
17 | _log = logging.get_logger()
18 |
19 | # 请求成功的返回码
20 | HTTP_OK_STATUS = [200, 202, 204]
21 |
22 |
23 | class _FormData(FormData):
24 | def _gen_form_data(self) -> multipart.MultipartWriter:
25 | """Encode a list of fields using the multipart/form-data MIME format"""
26 | if self._is_processed:
27 | return self._writer # rewrite this part of FormData object to enable retry of request
28 | for dispparams, headers, value in self._fields:
29 | try:
30 | if hdrs.CONTENT_TYPE in headers:
31 | part = payload.get_payload(
32 | value,
33 | content_type=headers[hdrs.CONTENT_TYPE],
34 | headers=headers,
35 | encoding=self._charset,
36 | )
37 | else:
38 | part = payload.get_payload(
39 | value,
40 | headers=headers,
41 | encoding=self._charset,
42 | )
43 | except Exception as exc:
44 | print(value)
45 | raise TypeError(
46 | "Can not serialize value type: %r\n " "headers: %r\n value: %r" % (type(value), headers, value)
47 | ) from exc
48 |
49 | if dispparams:
50 | part.set_content_disposition(
51 | "form-data",
52 | quote_fields=self._quote_fields,
53 | **dispparams,
54 | )
55 | assert part.headers is not None
56 | part.headers.popall(hdrs.CONTENT_LENGTH, None)
57 |
58 | self._writer.append_payload(part)
59 |
60 | self._is_processed = True
61 | return self._writer
62 |
63 |
64 | async def _handle_response(response: ClientResponse) -> Union[Dict[str, Any], str]:
65 | url = response.request_info.url
66 | try:
67 | condition = response.headers["content-type"] == "application/json"
68 | # note that when content-type is application/json, aiohttp will directly auto-sub encoding to be utf-8
69 | data = await response.json() if condition else await response.text()
70 | except (KeyError, JSONDecodeError):
71 | data = None
72 | if response.status in HTTP_OK_STATUS:
73 | _log.debug(f"[botpy] 请求成功, 请求连接: {url}, 返回内容: {data}, trace_id:{response.headers.get(X_TPS_TRACE_ID)}")
74 | return data
75 | else:
76 | _log.error(
77 | f"[botpy] 接口请求异常,请求连接: {url}, "
78 | f"错误代码: {response.status}, 返回内容: {data}, trace_id:{response.headers.get(X_TPS_TRACE_ID)}"
79 | # trace_id 用于定位接口问题
80 | )
81 | error_dict_get = HttpErrorDict.get(response.status)
82 | # type of data should be dict or str or None, so there should be a condition to check and prevent bug
83 | message = data["message"] if isinstance(data, dict) else str(data)
84 | if not error_dict_get:
85 | raise ServerError(message) from None # adding from None to prevent chain exception being raised
86 | raise error_dict_get(msg=message) from None
87 |
88 |
89 | class Route:
90 | DOMAIN: ClassVar[str] = "api.sgroup.qq.com"
91 | SANDBOX_DOMAIN: ClassVar[str] = "sandbox.api.sgroup.qq.com"
92 | SCHEME: ClassVar[str] = "https"
93 |
94 | def __init__(self, method: str, path: str, is_sandbox: str = False, **parameters: Any) -> None:
95 | self.method: str = method
96 | self.path: str = path
97 | self.is_sandbox = is_sandbox
98 | self.parameters = parameters
99 |
100 | @property
101 | def url(self):
102 | if self.is_sandbox:
103 | d = self.SANDBOX_DOMAIN
104 | else:
105 | d = self.DOMAIN
106 | _url = "{}://{}{}".format(self.SCHEME, d, self.path)
107 |
108 | # path的参数:
109 | if self.parameters:
110 | _url = _url.format_map(self.parameters)
111 | return _url
112 |
113 |
114 | class BotHttp:
115 | """
116 | TODO 增加请求重试功能 @veehou
117 | TODO 增加并发请求的锁控制 @veehou
118 | """
119 |
120 | def __init__(
121 | self,
122 | timeout: int,
123 | is_sandbox: bool = False,
124 | app_id: str = None,
125 | secret: str = None,
126 | ):
127 | self.timeout = timeout
128 | self.is_sandbox = is_sandbox
129 |
130 | self._token: Optional[Token] = None if not app_id else Token(app_id=app_id, secret=secret)
131 | self._session: Optional[aiohttp.ClientSession] = None
132 | self._global_over: Optional[asyncio.Event] = None
133 | self._headers: Optional[dict] = None
134 |
135 | def __del__(self):
136 | if self._session and not self._session.closed:
137 | _loop = asyncio.get_event_loop()
138 | _loop.create_task(self._session.close())
139 |
140 | async def close(self) -> None:
141 | if self._session and not self._session.closed:
142 | await self._session.close()
143 |
144 | async def check_session(self):
145 | await self._token.check_token()
146 | self._headers = {
147 | "Authorization": self._token.get_string(),
148 | "X-Union-Appid": self._token.app_id,
149 | }
150 |
151 | if not self._session or self._session.closed:
152 | self._session = aiohttp.ClientSession(
153 | connector=TCPConnector(limit=500, ssl=SSLContext(), force_close=True),
154 | )
155 |
156 | async def request(self, route: Route, retry_time: int = 0, **kwargs: Any):
157 | if retry_time > 2:
158 | return
159 | # some checking if it's a JSON request
160 | if "json" in kwargs:
161 | json_ = kwargs["json"]
162 | json__get = json_.get("file_image")
163 | if json__get and isinstance(json__get, bytes):
164 | kwargs["data"] = _FormData()
165 | for k, v in kwargs.pop("json").items():
166 | if v:
167 | if isinstance(v, dict):
168 | if k == "message_reference":
169 | _log.error(
170 | f"[botpy] 接口参数传入异常, 请求连接: {route.url}, "
171 | f"错误原因: file_image与message_reference不能同时传入,"
172 | f"备注: sdk已按照优先级,去除message_reference参数"
173 | )
174 | else:
175 | kwargs["data"].add_field(k, v)
176 |
177 | await self.check_session()
178 | route.is_sandbox = self.is_sandbox
179 | _log.debug(f"[botpy] 请求头部: {self._headers}, 请求方式: {route.method}, 请求url: {route.url}")
180 | _log.debug(self._session)
181 | try:
182 | async with self._session.request(
183 | method=route.method,
184 | url=route.url,
185 | headers=self._headers,
186 | timeout=(aiohttp.ClientTimeout(total=self.timeout)),
187 | **kwargs,
188 | ) as response:
189 | _log.debug(response)
190 | return await _handle_response(response)
191 | except asyncio.TimeoutError:
192 | _log.warning(f"请求超时,请求连接: {route.url}")
193 | except ConnectionResetError:
194 | _log.debug("session connection broken retry")
195 | await self.request(route, retry_time + 1, **kwargs)
196 |
197 | async def login(self, token: Token) -> robot.Robot:
198 | """login后保存token和session"""
199 |
200 | self._token = token
201 | await self.check_session()
202 | self._global_over = asyncio.Event()
203 | self._global_over.set()
204 |
205 | data = await self.request(Route("GET", "/users/@me"))
206 | # TODO 检查机器人token错误的raise exception @veehou
207 | return data
208 |
--------------------------------------------------------------------------------
/botpy/interaction.py:
--------------------------------------------------------------------------------
1 | from .api import BotAPI
2 | from .types import interaction
3 |
4 |
5 | class Interaction:
6 | __slots__ = (
7 | "_api",
8 | "_ctx",
9 | "id",
10 | "application_id",
11 | "type",
12 | "scene",
13 | "chat_type",
14 | "event_id",
15 | "data",
16 | "guild_id",
17 | "channel_id",
18 | "user_openid",
19 | "group_openid",
20 | "group_member_openid",
21 | "timestamp",
22 | "version",
23 | )
24 |
25 | def __init__(self, api: BotAPI, event_id, data: interaction.InteractionPayload):
26 | self._api = api
27 |
28 | self.id = data.get("id", None)
29 | self.type = data.get("type", None)
30 | self.scene = data.get("scene", None)
31 | self.chat_type = data.get("chat_type", None)
32 | self.application_id = data.get("application_id", None)
33 | self.event_id = event_id
34 | self.data = self._Data(data.get("data", {}))
35 | self.guild_id = data.get("guild_id", None)
36 | self.channel_id = data.get("channel_id", None)
37 | self.user_openid = data.get("user_openid", None)
38 | self.group_openid = data.get("group_openid", None)
39 | self.group_member_openid = data.get("group_member_openid", None)
40 | self.timestamp = data.get("timestamp", None)
41 | self.version = data.get("version", None)
42 |
43 | def __repr__(self):
44 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith("_")})
45 |
46 | class _Data:
47 | def __init__(self, data):
48 | self.type = data.get("type", None)
49 | self.resolved = Interaction._Resolved(data.get("resolved", None))
50 |
51 | def __repr__(self):
52 | return str(self.__dict__)
53 |
54 | class _Resolved:
55 | def __init__(self, data):
56 | self.button_id = data.get("button_id", None)
57 | self.button_data = data.get("button_data", None)
58 | self.message_id = data.get("message_id", None)
59 | self.user_id = data.get("user_id", None)
60 | self.feature_id = data.get("feature_id", None)
61 |
62 | def __repr__(self):
63 | return str(self.__dict__)
64 |
--------------------------------------------------------------------------------
/botpy/logging.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | import sys
5 | import json
6 | import yaml
7 | import logging
8 | import logging.config
9 | from typing import List, Dict, Union
10 | from logging.handlers import TimedRotatingFileHandler
11 |
12 | LOG_COLORS_CONFIG = {
13 | "DEBUG": "cyan",
14 | "INFO": "green",
15 | "WARNING": "yellow",
16 | "ERROR": "red",
17 | "CRITICAL": "red",
18 | }
19 |
20 | DEFAULT_LOGGER_NAME = "botpy"
21 |
22 | DEFAULT_PRINT_FORMAT = "\033[1;33m[%(levelname)s]\t(%(filename)s:%(lineno)s)%(funcName)s\t\033[0m%(message)s"
23 | DEFAULT_FILE_FORMAT = "%(asctime)s\t[%(levelname)s]\t(%(filename)s:%(lineno)s)%(funcName)s\t%(message)s"
24 | logging.basicConfig(format=DEFAULT_PRINT_FORMAT)
25 |
26 | DEFAULT_FILE_HANDLER = {
27 | # 要实例化的Handler
28 | "handler": TimedRotatingFileHandler,
29 | # 可选 Default to DEFAULT_FILE_FORMAT
30 | "format": "%(asctime)s\t[%(levelname)s]\t(%(filename)s:%(lineno)s)%(funcName)s\t%(message)s",
31 | # 可选 Default to DEBUG
32 | "level": logging.DEBUG,
33 | # 以下是Handler相关参数
34 | "when": "D",
35 | "backupCount": 7,
36 | "encoding": "utf-8",
37 | # *特殊* 对于filename参数,其中如有 %(name)s 会在实例化阶段填入相应的日志name
38 | "filename": os.path.join(os.getcwd(), "%(name)s.log"),
39 | }
40 |
41 | # 存放已经获取的Logger
42 | logs: Dict[str, logging.Logger] = {}
43 |
44 | # 追加的handler
45 | _ext_handlers: List[dict] = []
46 |
47 | # 解决Windows系统cmd运行日志输出不会显示颜色问题
48 | os.system("")
49 |
50 |
51 | def get_handler(handler, name=DEFAULT_LOGGER_NAME):
52 | """
53 | 将handler字典实例化
54 | :param handler: handler配置
55 | :param name: 动态路径参数
56 | :return: Handler
57 | """
58 | handler = handler.copy()
59 | if "filename" in handler:
60 | handler["filename"] = handler["filename"] % {"name": name}
61 |
62 | lever = handler.get("level") or logging.DEBUG
63 | _format = handler.get("format") or DEFAULT_FILE_FORMAT
64 |
65 | for k in ["level", "format"]:
66 | if k in handler:
67 | handler.pop(k)
68 |
69 | handler = handler.pop("handler")(**handler)
70 | handler.setLevel(lever)
71 | handler.setFormatter(logging.Formatter(_format))
72 | return handler
73 |
74 |
75 | def get_logger(name=None):
76 | global logs
77 |
78 | if not name:
79 | name = DEFAULT_LOGGER_NAME
80 | if name in logs:
81 | return logs[name]
82 |
83 | logger = logging.getLogger(name)
84 | # 从用户命令行接收是否打印debug日志
85 | argv = sys.argv
86 | if "-d" in argv or "--debug" in argv:
87 | logger.setLevel(level=logging.DEBUG)
88 | else:
89 | logger.setLevel(logging.INFO)
90 |
91 | # 添加额外handler
92 | if _ext_handlers:
93 | for handler in _ext_handlers:
94 | logger.addHandler(get_handler(handler, name))
95 |
96 | logs[name] = logger
97 | return logger
98 |
99 |
100 | def configure_logging(
101 | config: Union[str, dict] = None,
102 | _format: str = None,
103 | level: int = None,
104 | bot_log: Union[bool, None] = True,
105 | ext_handlers: Union[dict, List, bool] = None,
106 | force: bool = False
107 | ) -> None:
108 | """
109 | 修改日志配置
110 | :param config: logging.config.dictConfig
111 | :param _format: logging.basicConfig(format=_format)
112 | :param level: 控制台输出level
113 | :param bot_log: 是否启用bot日志 True/启用 None/禁用拓展 False/禁用拓展+控制台输出
114 | :param ext_handlers: 额外的handler,格式参考 DEFAULT_FILE_HANDLER。Default to True(使用默认handler)
115 | :param force: 是否在已追加handler(_ext_handlers)不为空时继续追加(避免因多次实例化Client类导致重复添加)
116 | """
117 | global _ext_handlers
118 |
119 | if config is not None:
120 | if isinstance(config, dict):
121 | logging.config.dictConfig(config)
122 | elif config.endswith(".json"):
123 | with open(config) as file:
124 | loaded_config = json.load(file)
125 | logging.config.dictConfig(loaded_config)
126 | elif config.endswith((".yaml", ".yml")):
127 | with open(config) as file:
128 | loaded_config = yaml.safe_load(file)
129 | logging.config.dictConfig(loaded_config)
130 | else:
131 | # See the note about fileConfig() here:
132 | # https://docs.python.org/3/library/logging.config.html#configuration-file-format
133 | logging.config.fileConfig(
134 | config, disable_existing_loggers=False
135 | )
136 |
137 | if _format is not None:
138 | logging.basicConfig(format=_format)
139 |
140 | if level is not None:
141 | for name, logger in logs.items():
142 | logger.setLevel(level)
143 |
144 | if not bot_log:
145 | logger = logging.getLogger(DEFAULT_LOGGER_NAME)
146 | if bot_log is False:
147 | logger.propagate = False
148 | if DEFAULT_LOGGER_NAME in logs:
149 | logs.pop(DEFAULT_LOGGER_NAME)
150 |
151 | logger.handlers = []
152 |
153 | if ext_handlers and (not _ext_handlers or force):
154 | if ext_handlers is True:
155 | ext_handlers = [DEFAULT_FILE_HANDLER]
156 | elif not isinstance(ext_handlers, list):
157 | ext_handlers = [ext_handlers]
158 |
159 | _ext_handlers.extend(ext_handlers)
160 |
161 | for name, logger in logs.items():
162 | for handler in ext_handlers:
163 | logger.addHandler(get_handler(handler, name))
164 |
--------------------------------------------------------------------------------
/botpy/manage.py:
--------------------------------------------------------------------------------
1 | from typing import Dict
2 | from .api import BotAPI
3 |
4 |
5 | class GroupManageEvent:
6 | __slots__ = (
7 | "_api",
8 | "event_id",
9 | "timestamp",
10 | "group_openid",
11 | "op_member_openid",
12 | )
13 |
14 | def __init__(self, api: BotAPI, event_id, data: Dict):
15 | self._api = api
16 | self.event_id = event_id
17 | self.timestamp = data.get("timestamp", None)
18 | self.group_openid = data.get("group_openid", None)
19 | self.op_member_openid = data.get("op_member_openid", None)
20 |
21 | def __repr__(self):
22 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith("_")})
23 |
24 |
25 | class C2CManageEvent:
26 | __slots__ = (
27 | "_api",
28 | "event_id",
29 | "timestamp",
30 | "openid",
31 | )
32 |
33 | def __init__(self, api: BotAPI, event_id, data: Dict):
34 | self._api = api
35 | self.event_id = event_id
36 | self.timestamp = data.get("timestamp", None)
37 | self.openid = data.get("openid", None)
38 |
39 | def __repr__(self):
40 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith("_")})
41 |
--------------------------------------------------------------------------------
/botpy/message.py:
--------------------------------------------------------------------------------
1 | from .api import BotAPI
2 | from .types import gateway
3 |
4 |
5 | class Message:
6 | __slots__ = (
7 | "_api",
8 | "author",
9 | "content",
10 | "channel_id",
11 | "id",
12 | "guild_id",
13 | "member",
14 | "message_reference",
15 | "mentions",
16 | "attachments",
17 | "seq",
18 | "seq_in_channel",
19 | "timestamp",
20 | "event_id",
21 | )
22 |
23 | def __init__(self, api: BotAPI, event_id, data: gateway.MessagePayload):
24 | # TODO 创建一些实体类的数据缓存 @veehou
25 | self._api = api
26 |
27 | self.author = self._User(data.get("author", {}))
28 | self.channel_id = data.get("channel_id", None)
29 | self.id = data.get("id", None)
30 | self.content = data.get("content", None)
31 | self.guild_id = data.get("guild_id", None)
32 | self.member = self._Member(data.get("member", {}))
33 | self.message_reference = self._MessageRef(data.get("message_reference", {}))
34 | self.mentions = [self._User(items) for items in data.get("mentions", {})]
35 | self.attachments = [self._Attachments(items) for items in data.get("attachments", {})]
36 | self.seq = data.get("seq", None) # 全局消息序号
37 | self.seq_in_channel = data.get("seq_in_channel", None) # 子频道消息序号
38 | self.timestamp = data.get("timestamp", None)
39 | self.event_id = event_id
40 |
41 | def __repr__(self):
42 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith("_")})
43 |
44 | class _User:
45 | def __init__(self, data):
46 | self.id = data.get("id", None)
47 | self.username = data.get("username", None)
48 | self.bot = data.get("bot", None)
49 | self.avatar = data.get("avatar", None)
50 |
51 | def __repr__(self):
52 | return str(self.__dict__)
53 |
54 | class _Member:
55 | def __init__(self, data):
56 | self.nick = data.get("nick", None)
57 | self.roles = data.get("roles", None)
58 | self.joined_at = data.get("joined_at", None)
59 |
60 | def __repr__(self):
61 | return str(self.__dict__)
62 |
63 | class _MessageRef:
64 | def __init__(self, data):
65 | self.message_id = data.get("message_id", None)
66 |
67 | def __repr__(self):
68 | return str(self.__dict__)
69 |
70 | class _Attachments:
71 | def __init__(self, data):
72 | self.content_type = data.get("content_type", None)
73 | self.filename = data.get("filename", None)
74 | self.height = data.get("height", None)
75 | self.width = data.get("width", None)
76 | self.id = data.get("id", None)
77 | self.size = data.get("size", None)
78 | self.url = data.get("url", None)
79 |
80 | def __repr__(self):
81 | return str(self.__dict__)
82 |
83 | async def reply(self, **kwargs):
84 | return await self._api.post_message(channel_id=self.channel_id, msg_id=self.id, **kwargs)
85 |
86 |
87 | class DirectMessage:
88 | __slots__ = (
89 | "_api",
90 | "author",
91 | "content",
92 | "direct_message",
93 | "channel_id",
94 | "id",
95 | "guild_id",
96 | "member",
97 | "message_reference",
98 | "attachments",
99 | "seq",
100 | "seq_in_channel",
101 | "src_guild_id",
102 | "timestamp",
103 | "event_id",
104 | )
105 |
106 | def __init__(self, api: BotAPI, event_id, data: gateway.DirectMessagePayload):
107 | self._api = api
108 |
109 | self.author = self._User(data.get("author", {}))
110 | self.channel_id = data.get("channel_id", None)
111 | self.id = data.get("id", None)
112 | self.content = data.get("content", None)
113 | self.direct_message = data.get("direct_message", None)
114 | self.guild_id = data.get("guild_id", None)
115 | self.member = self._Member(data.get("member", {}))
116 | self.message_reference = self._MessageRef(data.get("message_reference", {}))
117 | self.attachments = [self._Attachments(items) for items in data.get("attachments", {})]
118 | self.seq = data.get("seq", None) # 全局消息序号
119 | self.seq_in_channel = data.get("seq_in_channel", None) # 子频道消息序号
120 | self.src_guild_id = data.get("src_guild_id", None)
121 | self.timestamp = data.get("timestamp", None)
122 | self.event_id = event_id
123 |
124 | def __repr__(self):
125 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith("_")})
126 |
127 | class _User:
128 | def __init__(self, data):
129 | self.id = data.get("id", None)
130 | self.username = data.get("username", None)
131 | self.avatar = data.get("avatar", None)
132 |
133 | def __repr__(self):
134 | return str(self.__dict__)
135 |
136 | class _Member:
137 | def __init__(self, data):
138 | self.joined_at = data.get("joined_at", None)
139 |
140 | def __repr__(self):
141 | return str(self.__dict__)
142 |
143 | class _MessageRef:
144 | def __init__(self, data):
145 | self.message_id = data.get("message_id", None)
146 |
147 | def __repr__(self):
148 | return str(self.__dict__)
149 |
150 | class _Attachments:
151 | def __init__(self, data):
152 | self.content_type = data.get("content_type", None)
153 | self.filename = data.get("filename", None)
154 | self.height = data.get("height", None)
155 | self.width = data.get("width", None)
156 | self.id = data.get("id", None)
157 | self.size = data.get("size", None)
158 | self.url = data.get("url", None)
159 |
160 | def __repr__(self):
161 | return str(self.__dict__)
162 |
163 | async def reply(self, **kwargs):
164 | return await self._api.post_dms(guild_id=self.guild_id, msg_id=self.id, **kwargs)
165 |
166 |
167 | class MessageAudit:
168 | __slots__ = (
169 | "_api",
170 | "audit_id",
171 | "message_id",
172 | "channel_id",
173 | "guild_id",
174 | "event_id",
175 | )
176 |
177 | def __init__(self, api: BotAPI, event_id, data: gateway.MessageAuditPayload):
178 | self._api = api
179 |
180 | self.audit_id = data.get("audit_id", None)
181 | self.channel_id = data.get("channel_id", None)
182 | self.message_id = data.get("message_id", None)
183 | self.guild_id = data.get("guild_id", None)
184 | self.event_id = event_id
185 |
186 | def __repr__(self):
187 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith("_")})
188 |
189 |
190 | class BaseMessage:
191 | __slots__ = (
192 | "_api",
193 | "content",
194 | "id",
195 | "message_reference",
196 | "mentions",
197 | "attachments",
198 | "msg_seq",
199 | "timestamp",
200 | "event_id",
201 | )
202 |
203 | def __init__(self, api: BotAPI, event_id, data: gateway.MessagePayload):
204 | self._api = api
205 | self.id = data.get("id", None)
206 | self.content = data.get("content", None)
207 | self.message_reference = self._MessageRef(data.get("message_reference", {}))
208 | self.mentions = [self._User(items) for items in data.get("mentions", {})]
209 | self.attachments = [self._Attachments(items) for items in data.get("attachments", {})]
210 | self.msg_seq = data.get("msg_seq", None) # 全局消息序号
211 | self.timestamp = data.get("timestamp", None)
212 | self.event_id = event_id
213 |
214 | def __repr__(self):
215 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith("_")})
216 |
217 | class _MessageRef:
218 | def __init__(self, data):
219 | self.message_id = data.get("message_id", None)
220 |
221 | def __repr__(self):
222 | return str(self.__dict__)
223 |
224 | class _Attachments:
225 | def __init__(self, data):
226 | self.content_type = data.get("content_type", None)
227 | self.filename = data.get("filename", None)
228 | self.height = data.get("height", None)
229 | self.width = data.get("width", None)
230 | self.id = data.get("id", None)
231 | self.size = data.get("size", None)
232 | self.url = data.get("url", None)
233 |
234 | def __repr__(self):
235 | return str(self.__dict__)
236 |
237 |
238 | class GroupMessage(BaseMessage):
239 | __slots__ = (
240 | "author",
241 | "group_openid",
242 | )
243 |
244 | def __init__(self, api: BotAPI, event_id, data: gateway.MessagePayload):
245 | super().__init__(api, event_id, data)
246 | self.author = self._User(data.get("author", {}))
247 | self.group_openid = data.get("group_openid", None)
248 |
249 | def __repr__(self):
250 | slots = self.__slots__ + super().__slots__
251 | return str({items: str(getattr(self, items)) for items in slots if not items.startswith("_")})
252 |
253 | class _User:
254 | def __init__(self, data):
255 | self.member_openid = data.get("member_openid", None)
256 |
257 | def __repr__(self):
258 | return str(self.__dict__)
259 |
260 | async def reply(self, **kwargs):
261 | return await self._api.post_group_message(group_openid=self.group_openid, msg_id=self.id, **kwargs)
262 |
263 | class C2CMessage(BaseMessage):
264 | __slots__ = ("author",)
265 |
266 | def __init__(self, api: BotAPI, event_id, data: gateway.MessagePayload):
267 | super().__init__(api, event_id, data)
268 |
269 | self.author = self._User(data.get("author", {}))
270 |
271 | def __repr__(self):
272 | slots = self.__slots__ + super().__slots__
273 | return str({items: str(getattr(self, items)) for items in slots if not items.startswith("_")})
274 |
275 | class _User:
276 | def __init__(self, data):
277 | self.user_openid = data.get("user_openid", None)
278 |
279 | def __repr__(self):
280 | return str(self.__dict__)
281 |
282 | async def reply(self, **kwargs):
283 | return await self._api.post_c2c_message(openid=self.author.user_openid, msg_id=self.id, **kwargs)
284 |
--------------------------------------------------------------------------------
/botpy/reaction.py:
--------------------------------------------------------------------------------
1 | from .api import BotAPI
2 | from .types import reaction
3 |
4 |
5 | class Reaction:
6 | __slots__ = (
7 | "_api",
8 | "_ctx",
9 | "user_id",
10 | "channel_id",
11 | "guild_id",
12 | "emoji",
13 | "target",
14 | "event_id")
15 |
16 | def __init__(self, api: BotAPI, event_id, data: reaction.Reaction):
17 | self._api = api
18 |
19 | self.user_id = data.get("user_id", None)
20 | self.channel_id = data.get("channel_id", None)
21 | self.guild_id = data.get("guild_id", None)
22 | self.emoji = self._Emoji(data.get("emoji", {}))
23 | self.target = self._Target(data.get("target", {}))
24 | self.event_id = event_id
25 |
26 | def __repr__(self):
27 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith('_')})
28 |
29 | class _Emoji:
30 | def __init__(self, data):
31 | self.id = data.get("id", None)
32 | self.type = data.get("type", None)
33 |
34 | def __repr__(self):
35 | return str(self.__dict__)
36 |
37 | class _Target:
38 | def __init__(self, data):
39 | self.id = data.get("id", None)
40 | self.type = data.get("type", None) # 0: 消息 1: 帖子 2: 评论 3: 回复
41 |
42 | def __repr__(self):
43 | return str(self.__dict__)
44 |
--------------------------------------------------------------------------------
/botpy/robot.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import time
3 |
4 | import aiohttp
5 |
6 | from .logging import get_logger
7 | from botpy.types import robot
8 |
9 | _log = get_logger()
10 |
11 |
12 | class Robot:
13 | def __init__(self, data: robot.Robot):
14 | self._update(data)
15 |
16 | def _update(self, data: robot.Robot) -> None:
17 | self.name = data.get("username")
18 | self.id = int(data["id"])
19 | self.avatar = data.get("avatar")
20 |
21 |
22 | class Token:
23 | TYPE_BOT = "QQBot"
24 | TYPE_NORMAL = "Bearer"
25 |
26 | def __init__(self, app_id: str, secret: str):
27 | """
28 | :param app_id:
29 | 机器人appid
30 | :param secret:
31 | 机器人密钥
32 | """
33 | self.app_id = app_id
34 | self.secret = secret
35 | self.access_token = None
36 | self.expires_in = 0
37 | self.Type = self.TYPE_BOT
38 |
39 | async def check_token(self):
40 | if self.access_token is None or int(time.time()) >= self.expires_in:
41 | await self.update_access_token()
42 |
43 | async def update_access_token(self):
44 | session = aiohttp.ClientSession()
45 | data = None
46 | # TODO 增加超时重试
47 | try:
48 | async with session.post(
49 | url="https://bots.qq.com/app/getAppAccessToken",
50 | timeout=(aiohttp.ClientTimeout(total=20)),
51 | json={
52 | "appId": self.app_id,
53 | "clientSecret": self.secret,
54 | },
55 | ) as response:
56 | data = await response.json()
57 | except asyncio.TimeoutError as e:
58 | _log.info("[botpy] access_token TimeoutError:" + str(e))
59 | raise
60 | finally:
61 | await session.close()
62 | if "access_token" not in data or "expires_in" not in data:
63 | _log.error("[botpy] 获取token失败,请检查appid和secret填写是否正确!")
64 | raise RuntimeError(str(data))
65 | _log.info("[botpy] access_token expires_in " + data["expires_in"])
66 | self.access_token = data["access_token"]
67 | self.expires_in = int(data["expires_in"]) + int(time.time())
68 |
69 | # BotToken 机器人身份的 token
70 | def bot_token(self):
71 | return self
72 |
73 | # GetString 获取授权头字符串
74 | def get_string(self):
75 | if self.Type == self.TYPE_NORMAL:
76 | return self.access_token
77 | return "{} {}".format(self.Type, self.access_token)
78 |
79 | def get_type(self):
80 | return self.Type
81 |
--------------------------------------------------------------------------------
/botpy/types/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | 这里放一些payload或者数据传输类的实体
4 | 主要通过TypedDict来限定字段和类型
5 | """
6 |
--------------------------------------------------------------------------------
/botpy/types/announce.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from enum import Enum
3 | from typing import List, TypedDict
4 |
5 |
6 | class RecommendChannel(TypedDict):
7 | channel_id: str
8 | introduce: str
9 |
10 |
11 | class AnnouncesType(Enum):
12 | MEMBER = 0 # 成员公告
13 | WELCOME = 1 # 欢迎公告
14 |
15 | def __int__(self) -> int:
16 | return self.value
17 |
18 |
19 | class Announce(TypedDict):
20 | guild_id: str
21 | channel_id: str
22 | message_id: str
23 | announces_type: AnnouncesType
24 | recommend_channels: List[RecommendChannel]
25 |
--------------------------------------------------------------------------------
/botpy/types/audio.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import TypedDict, Literal
3 |
4 |
5 | """
6 | START = 0
7 | PAUSE = 1
8 | RESUME = 2
9 | STOP = 3
10 | """
11 | AudioStatus = Literal[0, 1, 2, 3]
12 |
13 | PublicAudioType = Literal[2, 5]
14 |
15 | class AudioControl(TypedDict):
16 | audio_url: str
17 | text: str
18 | status: AudioStatus
19 |
20 |
21 | class AudioAction(TypedDict):
22 | guild_id: str
23 | channel_id: str
24 | audio_url: str
25 | text: str
26 |
27 |
28 | class AudioLive(TypedDict):
29 | guild_id: str
30 | channel_id: str
31 | channel_type: PublicAudioType
32 | user_id: str
33 |
--------------------------------------------------------------------------------
/botpy/types/channel.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from enum import Enum
3 | from typing import TypedDict
4 |
5 |
6 | class ChannelType(Enum):
7 | TEXT_CHANNEL = 0 # 文字子频道
8 | # x_CHANNEL = 1 # 保留,不可用
9 | VOICE_CHANNEL = 2 # 语音子频道
10 | # x_CHANNEL = 3 # 保留,不可用
11 | GROUP_CHANNEL = 4 # 子频道分组
12 | LIVE_CHANNEL = 10005 # 直播子频道
13 | APP_CHANNEL = 10006 # 应用子频道
14 | DISCUSSION_CHANNEL = 10007 # 论坛子频道
15 |
16 | def __int__(self) -> int:
17 | return self.value
18 |
19 |
20 | class ChannelSubType(Enum):
21 | TALK = 0 # 闲聊
22 | POST = 1 # 公告
23 | CHEAT = 2 # 攻略
24 | BLACK = 3 # 开黑
25 |
26 | def __int__(self) -> int:
27 | return self.value
28 |
29 |
30 | class PrivateType(Enum):
31 | PUBLIC = 0 # 公开频道
32 | ADMIN = 1 # 管理员和群主可见
33 | SPECIFIED_USER = 2 # 群主管理员+指定成员,可使用 修改子频道权限接口 指定成员
34 |
35 | def __int__(self) -> int:
36 | return self.value
37 |
38 |
39 | class SpeakPermission(Enum):
40 | INVALID = 0 # 无效类型
41 | EVERYONE = 1 # 所有人
42 | ADMIN = 2 # 群主管理员+指定成员,可使用 修改子频道权限接口 指定成员
43 |
44 | def __int__(self) -> int:
45 | return self.value
46 |
47 |
48 | class ChannelPayload(TypedDict):
49 | id: str
50 | guild_id: str
51 | name: str
52 | type: ChannelType
53 | sub_type: ChannelSubType
54 | position: int
55 | parent_id: str
56 | owner_id: str
57 | private_type: PrivateType
58 | speak_permission: SpeakPermission
59 | application_id: str
60 | permissions: str
61 |
62 |
63 | class ChannelPermissions(TypedDict):
64 | channel_id: str
65 | user_id: str
66 | permissions: str
67 | role_id: str
68 |
--------------------------------------------------------------------------------
/botpy/types/emoji.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import Literal, TypedDict
3 |
4 | """
5 | EmojiType
6 | 值 描述
7 | 1 系统表情
8 | 2 emoji表情
9 | """
10 | EmojiType = Literal[1, 2]
11 |
12 |
13 | class Emoji(TypedDict):
14 | id: str
15 | type: EmojiType
16 |
--------------------------------------------------------------------------------
/botpy/types/forum.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict, Literal, List
2 | from botpy.types.rich_text import AuditType
3 |
4 | # 1:普通文本 2:HTML 3:Markdown 4: json
5 | Format = Literal[1, 2, 3, 4]
6 |
7 |
8 | class ThreadInfo(TypedDict):
9 | thread_id: str
10 | title: str
11 | content: str
12 | date_time: str
13 |
14 |
15 | class Thread(TypedDict):
16 | guild_id: str
17 | channel_id: str
18 | author_id: str
19 | thread_info: ThreadInfo
20 |
21 |
22 | class PostInfo(TypedDict):
23 | thread_id: str
24 | post_id: str
25 | content: str
26 | date_time: str
27 |
28 |
29 | class Post(TypedDict):
30 | guild_id: str
31 | channel_id: str
32 | author_id: str
33 | post_info: PostInfo
34 |
35 |
36 | class ReplyInfo(TypedDict):
37 | thread_id: str
38 | post_id: str
39 | reply_id: str
40 | content: str
41 | date_time: str
42 |
43 |
44 | class Reply(TypedDict):
45 | guild_id: str
46 | channel_id: str
47 | author_id: str
48 | reply_info: ReplyInfo
49 |
50 |
51 | class AuditResult(TypedDict):
52 | guild_id: str
53 | channel_id: str
54 | author_id: str
55 | thread_id: str
56 | post_id: str
57 | reply_id: str
58 | type: AuditType
59 | result: int
60 | err_msg: str
61 |
62 |
63 | class ForumRsp(TypedDict):
64 | threads: List[Thread]
65 | is_finish: int
66 |
67 |
68 | class PostThreadRsp(TypedDict):
69 | task_id: str
70 | create_time: str
71 |
72 | class OpenForumEvent(TypedDict):
73 | guild_id: str
74 | channel_id: str
75 | author_id: str
76 |
--------------------------------------------------------------------------------
/botpy/types/gateway.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict, List
2 |
3 | from .user import Member
4 |
5 |
6 | class WsContext(TypedDict):
7 | id: str # 被动事件里携带的上下文信息,目前仅有部分事件支持
8 |
9 |
10 | class UserPayload(TypedDict):
11 | id: str
12 | username: str
13 | bot: bool
14 | status: int
15 | avatar: str
16 |
17 |
18 | class MessageRefPayload(TypedDict):
19 | message_id: str
20 |
21 |
22 | class MessageAttachPayload(TypedDict):
23 | content_type: str
24 | filename: str
25 | height: int
26 | width: int
27 | id: str
28 | size: int
29 | url: str
30 |
31 |
32 | class ReadyEvent(TypedDict):
33 | version: int
34 | session_id: str
35 | user: UserPayload
36 | shard: List[int]
37 |
38 |
39 | class WsUrlPayload(TypedDict):
40 | url: str
41 |
42 |
43 | class MessagePayload(TypedDict):
44 | author: UserPayload
45 | channel_id: str
46 | content: str
47 | guild_id: str
48 | id: str
49 | member: Member
50 | message_reference: MessageRefPayload
51 | mentions: List[UserPayload]
52 | attachments: List[MessageAttachPayload]
53 | seq: int
54 | seq_in_channel: str
55 | timestamp: str
56 |
57 |
58 | class DirectMessagePayload(TypedDict):
59 | author: UserPayload
60 | channel_id: str
61 | content: str
62 | direct_message: bool
63 | guild_id: str
64 | id: str
65 | member: Member
66 | message_reference: MessageRefPayload
67 | attachments: List[MessageAttachPayload]
68 | seq: int
69 | seq_in_channel: str
70 | src_guild_id: str
71 | timestamp: str
72 |
73 |
74 | class MessageAuditPayload(TypedDict):
75 | audit_id: str
76 | message_id: str
77 | guild_id: str
78 | channel_id: str
79 | audit_time: str
80 | create_time: str
81 | seq_in_channel: str
82 |
--------------------------------------------------------------------------------
/botpy/types/guild.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import TypedDict, List
3 |
4 | from botpy.types.user import User
5 |
6 |
7 | class GuildPayload(TypedDict):
8 | id: str
9 | name: str
10 | icon: str
11 | owner_id: str
12 | owner: bool
13 | member_count: int
14 | max_members: int
15 | description: str
16 | joined_at: str
17 |
18 |
19 | class Role(TypedDict):
20 | id: str
21 | name: str
22 | color: int
23 | hoist: int
24 | number: int
25 | number_limit: int
26 |
27 |
28 | class GuildRole(TypedDict):
29 | guild_id: str
30 | role_id: str
31 | role: Role
32 |
33 |
34 | class GuildRoles(TypedDict):
35 | guild_id: str
36 | roles: List[Role]
37 | role_num_limit: str
38 |
39 |
40 | class GuildMembers(TypedDict):
41 | user: List[User]
42 | nick: str
43 | roles: List[Role]
44 | joined_at: str
45 |
--------------------------------------------------------------------------------
/botpy/types/inline.py:
--------------------------------------------------------------------------------
1 | from typing import List, TypedDict
2 |
3 |
4 | class Permission(TypedDict):
5 | type: int
6 | specify_role_ids: List[str]
7 | specify_user_ids: List[str]
8 |
9 |
10 | class RenderData(TypedDict):
11 | label: str
12 | visited_label: str
13 | style: int
14 |
15 |
16 | class Action(TypedDict):
17 | type: int
18 | permission: Permission
19 | click_limit: int
20 | data: str
21 | at_bot_show_channel_list: bool
22 |
23 |
24 | class Button(TypedDict):
25 | id: str
26 | render_data: RenderData
27 | action: Action
28 |
29 |
30 | class KeyboardRow(TypedDict):
31 | buttons: List[Button]
32 |
33 |
34 | class Keyboard(TypedDict):
35 | rows: List[KeyboardRow]
36 |
--------------------------------------------------------------------------------
/botpy/types/interaction.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import TypedDict
3 |
4 |
5 | class InteractionData:
6 | type: int
7 | resolved: object
8 |
9 |
10 | class InteractionPayload(TypedDict):
11 | id: str
12 | application_id: int
13 | type: int
14 | scene: str
15 | chat_type: int
16 | data: InteractionData
17 | guild_id: int
18 | channel_id: int
19 | user_openid: str
20 | group_openid: str
21 | group_member_openid: str
22 | timestamp: int
23 | version: int
24 |
25 |
26 | class InteractionType(Enum):
27 | PING = 1
28 | APPLICATION_COMMAND = 2
29 | HTTP_PROXY = 10
30 | INLINE_KEYBOARD = 11
31 |
32 | def __int__(self) -> int:
33 | return self.value
34 |
35 |
36 | class InteractionDataType(Enum):
37 | CHAT_INPUT_SEARCH = 9
38 | HTTP_PROXY = 10
39 | INLINE_KEYBOARD_BUTTON_CLICK = 11
40 |
41 | def __int__(self) -> int:
42 | return self.value
43 |
--------------------------------------------------------------------------------
/botpy/types/message.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from enum import Enum
3 | from typing import List, TypedDict
4 |
5 | from .gateway import MessagePayload
6 | from .inline import Keyboard
7 |
8 |
9 | class Attachment(TypedDict):
10 | url: str
11 |
12 |
13 | class Thumbnail(TypedDict):
14 | url: str # 图片地址
15 |
16 |
17 | class EmbedField(TypedDict):
18 | name: str
19 |
20 |
21 | class Embed(TypedDict, total=False):
22 | title: str # 标题
23 | prompt: str # 消息弹窗内容
24 | thumbnail: Thumbnail # 缩略图
25 | fields: List[EmbedField] # 消息创建时间
26 |
27 |
28 | class ArkObjKv(TypedDict):
29 | key: str
30 | value: str
31 |
32 |
33 | class ArkObj(TypedDict):
34 | obj_kv: List[ArkObjKv]
35 |
36 |
37 | class ArkKv(TypedDict, total=False):
38 | key: str
39 | value: str
40 | obj: List[ArkObj]
41 |
42 |
43 | class Ark(TypedDict):
44 | template_id: int
45 | kv: List[ArkKv]
46 |
47 |
48 | class Reference(TypedDict):
49 | message_id: str
50 | ignore_get_message_error: bool
51 |
52 |
53 | class MessageMarkdownParams(TypedDict):
54 | key: str
55 | values: List[str]
56 |
57 |
58 | class MarkdownPayload(TypedDict, total=False):
59 | custom_template_id: str
60 | params: List[MessageMarkdownParams]
61 | content: str
62 |
63 |
64 | class KeyboardPayload(TypedDict, total=False):
65 | id: str
66 | content: Keyboard
67 |
68 | class Media(TypedDict):
69 | file_uuid: str # 文件ID
70 | file_info: str # 文件信息,用于发消息接口的media字段使用
71 | ttl: int # 有效期,标识剩余多少秒到期,到期后 file_info 失效,当等于 0 时,表示可长期使用
72 |
73 |
74 | class Message(MessagePayload):
75 | edited_timestamp: str
76 | mention_everyone: str
77 | attachments: List[Attachment]
78 | embeds: List[Embed]
79 | ark: Ark
80 | message_reference: Reference
81 | markdown: MarkdownPayload
82 | keyboard: KeyboardPayload
83 |
84 |
85 | class TypesEnum(Enum):
86 | around = "around"
87 | before = "before"
88 | after = "after"
89 | latest = ""
90 |
91 |
92 | class MessagesPager(TypedDict):
93 | type: TypesEnum
94 | id: str
95 | limit: str
96 |
97 |
98 | class DmsPayload(TypedDict):
99 | guild_id: str # 注意,这里是私信会话的guild_id, 每个私信会话居然是个单独的guild
100 | channel_id: str
101 | creat_time: str
102 |
103 |
104 | class DMOriginalAuthor(TypedDict):
105 | id: str
106 | username: str
107 | bot: bool
108 |
109 |
110 | class DeletedMessage(TypedDict):
111 | guild_id: str
112 | channel_id: str
113 | id: str
114 | author: DMOriginalAuthor
115 |
116 |
117 | class DeletionOperator(TypedDict):
118 | id: str
119 |
120 |
121 | class DeletedMessageInfo(TypedDict):
122 | message: DeletedMessage
123 | op_user: DeletionOperator
124 |
--------------------------------------------------------------------------------
/botpy/types/permission.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import TypedDict
3 |
4 |
5 | class APIPermission(TypedDict):
6 | path: str
7 | method: str
8 | desc: str
9 | auth_status: int
10 |
11 |
12 | class APIPermissionDemandIdentify(TypedDict):
13 | path: str
14 | method: str
15 |
16 |
17 | class APIPermissionDemand(TypedDict):
18 | guild_id: str
19 | channel_id: str
20 | api_identify: APIPermissionDemandIdentify
21 | title: str
22 | desc: str
23 |
--------------------------------------------------------------------------------
/botpy/types/pins_message.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import List
3 |
4 |
5 | class PinsMessage:
6 | guild_id: str
7 | channel_id: str
8 | message_ids: List[str]
9 |
--------------------------------------------------------------------------------
/botpy/types/reaction.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict, Literal, List
2 |
3 | from .emoji import Emoji
4 | from .user import User
5 |
6 | # 0: 消息 1: 帖子 2: 评论 3: 回复
7 | ReactionTargetType = Literal[0, 1, 2, 3]
8 |
9 |
10 | class ReactionTarget(TypedDict):
11 | id: str
12 | type: ReactionTargetType
13 |
14 |
15 | class Reaction(TypedDict):
16 | user_id: str
17 | guild_id: str
18 | channel_id: str
19 | target: ReactionTarget
20 | emoji: Emoji
21 |
22 |
23 | class ReactionUsers(TypedDict):
24 | users: List[User]
25 | cookie: str # 分页参数,用于拉取下一页
26 | is_end: bool # 是否已拉取完成到最后一页,true代表完成
27 |
--------------------------------------------------------------------------------
/botpy/types/rich_text.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict, Literal, List
2 |
3 | # 1:帖子 2:评论 3:回复
4 | AuditType = Literal[1, 2, 3]
5 | # 1:普通文本 2:at信息 3:url信息 4:表情 5:#子频道 10:视频 11:图片
6 | RichType = Literal[1, 2, 3, 4, 5, 10, 11]
7 | # 1:at特定的人 2:at角色组所有人 3:at频道所有人
8 | AtType = Literal[1, 2, 3]
9 | # 1:文本 2:图片 3:视频 4:url
10 | ElemType = Literal[1, 2, 3, 4]
11 | # 0:左对齐 1:居中 2:右对齐
12 | Alignment = Literal[0, 1, 2]
13 |
14 |
15 | class ParagraphProps(TypedDict):
16 | alignment: Alignment
17 |
18 |
19 | class URLElem(TypedDict):
20 | url: str
21 | desc: str
22 |
23 |
24 | class ImageElem(TypedDict):
25 | third_url: str
26 | width_percent: float
27 |
28 |
29 | class PlatImage(TypedDict):
30 | url: str
31 | width: int
32 | height: int
33 | image_id: str
34 |
35 |
36 | class VideoElem(TypedDict):
37 | third_url: str
38 |
39 |
40 | class PlatVideo(TypedDict):
41 | url: str
42 | width: int
43 | height: int
44 | video_id: str
45 | duration: int
46 | cover: PlatImage
47 |
48 |
49 | class TextProps(TypedDict):
50 | font_bold: bool
51 | italic: bool
52 | underline: bool
53 |
54 |
55 | class TextElem(TypedDict):
56 | text: str
57 | props: TextProps
58 |
59 |
60 | class Elem(TypedDict):
61 | text: TextElem
62 | image: ImageElem
63 | video: VideoElem
64 | url: URLElem
65 | type: ElemType
66 |
67 |
68 | class Paragraph(TypedDict):
69 | elems: List[Elem]
70 | props: ParagraphProps
71 |
72 |
73 | class RichText(TypedDict):
74 | paragraphs: Paragraph
75 |
76 |
77 | class ChannelInfo(TypedDict):
78 | channel_id: int
79 | channel_name: str
80 |
81 |
82 | class EmojiInfo(TypedDict):
83 | id: str
84 | type: str
85 | name: str
86 | url: str
87 |
88 |
89 | class URLInfo(TypedDict):
90 | url: str
91 | display_text: str
92 |
93 |
94 | class AtGuildInfo(TypedDict):
95 | guild_id: str
96 | guild_name: str
97 |
98 |
99 | class AtRoleInfo(TypedDict):
100 | role_id: int
101 | name: str
102 | color: int
103 |
104 |
105 | class AtUserInfo(TypedDict):
106 | id: str
107 | nick: str
108 |
109 |
110 | class AtInfo(TypedDict):
111 | type: AuditType
112 | user_info: AtUserInfo
113 | role_info: AtRoleInfo
114 | guild_info: AtGuildInfo
115 |
116 |
117 | class TextInfo(TypedDict):
118 | text: str
119 |
120 |
121 | class RichObject(TypedDict):
122 | type: RichType
123 | text_info: TextInfo
124 | at_info: AtInfo
125 | url_info: URLInfo
126 | emoji_info: EmojiInfo
127 | channel_info: ChannelInfo
128 |
129 |
--------------------------------------------------------------------------------
/botpy/types/robot.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict
2 |
3 |
4 | class Robot(TypedDict):
5 | id: str
6 | username: str
7 | avatar: str
8 |
--------------------------------------------------------------------------------
/botpy/types/schedule.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import Literal
3 |
4 | from botpy.types.user import Member
5 |
6 | RemindType = Literal[0, 1, 2, 3, 4, 5]
7 |
8 |
9 | class Schedule:
10 | """
11 | RemindType
12 | 提醒类型 id 描述
13 | 0 不提醒
14 | 1 开始时提醒
15 | 2 开始前 5 分钟提醒
16 | 3 开始前 15 分钟提醒
17 | 4 开始前 30 分钟提醒
18 | 5 开始前 60 分钟提醒
19 | """
20 |
21 | id: str
22 | name: str
23 | description: str
24 | start_timestamp: str
25 | end_timestamp: str
26 | creator: Member
27 | jump_channel_id: str
28 | remind_type: RemindType
29 |
--------------------------------------------------------------------------------
/botpy/types/session.py:
--------------------------------------------------------------------------------
1 | from typing import TypedDict
2 |
3 | from ..robot import Token
4 |
5 |
6 | class ShardConfig(TypedDict):
7 | shard_id: int
8 | shard_count: int
9 |
10 |
11 | class Session(TypedDict):
12 | session_id: str
13 | last_seq: int
14 | intent: int
15 | token: Token
16 | url: str
17 | shards: ShardConfig
18 |
--------------------------------------------------------------------------------
/botpy/types/user.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import TypedDict, List
3 |
4 |
5 | class User(TypedDict):
6 | id: str
7 | username: str
8 | avatar: str
9 | bot: bool
10 | union_openid: str
11 | union_user_account: str
12 |
13 |
14 | class Member(TypedDict):
15 | user: User
16 | nick: str
17 | roles: List[str]
18 | joined_at: str
19 |
20 |
21 | class GuildMemberPayload(Member):
22 | guild_id: str
23 |
--------------------------------------------------------------------------------
/botpy/user.py:
--------------------------------------------------------------------------------
1 | from .api import BotAPI
2 | from .types import user
3 |
4 |
5 | class Member:
6 | __slots__ = ("_api", "_ctx", "user", "nick", "roles", "joined_at", "event_id", "guild_id")
7 |
8 | def __init__(self, api: BotAPI, event_id, data: user.GuildMemberPayload):
9 | self._api = api
10 |
11 | self.user = self._User(data.get("user", {}))
12 | self.nick = data.get("nick", None)
13 | self.roles = data.get("roles", None)
14 | self.joined_at = data.get("joined_at", None)
15 | self.event_id = event_id
16 | self.guild_id = data.get("guild_id", None)
17 |
18 | def __repr__(self):
19 | return str({items: str(getattr(self, items)) for items in self.__slots__ if not items.startswith('_')})
20 |
21 | class _User:
22 | def __init__(self, data):
23 | self.id = data.get("id", None)
24 | self.username = data.get("username", None)
25 | self.avatar = data.get("avatar", None)
26 | self.bot = data.get("bot", None)
27 | self.union_openid = data.get("union_openid", None)
28 | self.union_user_account = data.get("union_user_account", None)
29 |
30 | def __repr__(self):
31 | return str(self.__dict__)
32 |
--------------------------------------------------------------------------------
/docs/20211216-QQ频道机器人分享-qqbot-python(open).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tencent-connect/botpy/e25f3e84bad7217357d8200a9d12939b58285b84/docs/20211216-QQ频道机器人分享-qqbot-python(open).pdf
--------------------------------------------------------------------------------
/docs/事件监听.md:
--------------------------------------------------------------------------------
1 | # 事件监听
2 |
3 | **本文档概述[botpy的事件监听](https://github.com/tencent-connect/botpy/blob/feature/botpy_1.0/botpy/flags.py)**
4 |
5 | ## 目录
6 |
7 | - [准备](#准备)
8 | - [使用方式](#使用方式)
9 | - [监听的事件](#监听的事件)
10 | - [公域消息事件的监听](#公域消息事件的监听)
11 | - [消息事件的监听](#消息事件的监听)
12 | - [私信事件的监听](#私信事件的监听)
13 | - [消息相关互动事件的监听](#消息相关互动事件的监听)
14 | - [频道事件的监听](#频道事件的监听)
15 | - [频道成员事件的监听](#频道成员事件的监听)
16 | - [互动事件的监听](#互动事件的监听)
17 | - [消息审核事件的监听](#消息审核事件的监听)
18 | - [论坛事件的监听](#论坛事件的监听)
19 | - [音频事件的监听](#音频事件的监听)
20 | - [音视频/直播子频道成员进出事件](#音视频/直播子频道成员进出事件)
21 | - [开放论坛事件对象](#开放论坛事件对象)
22 | - [订阅事件的方法](#订阅事件的方法)
23 |
24 | ## 准备
25 |
26 | ```python
27 | import botpy
28 | ```
29 |
30 | ## 使用方式
31 |
32 | 通过继承实现`bot.Client`, 实现自己的机器人Client
33 |
34 | 在`MyClient`类中调用函数实现处理事件的功能
35 |
36 | ```python
37 | class MyClient(botpy.Client):
38 | ```
39 |
40 | ## 监听的事件
41 |
42 | ### 公域消息事件的监听
43 |
44 | 首先需要订阅事件`public_guild_messages`
45 |
46 | ```python
47 | intents = botpy.Intents(public_guild_messages=True)
48 | client = MyClient(intents=intents)
49 | ```
50 |
51 | | 对应函数 | 说明 |
52 | | ------------------------------------------------ | ----------- |
53 | | on_at_message_create(self, message: Message) | 当收到@机器人的消息时 |
54 | | on_public_message_delete(self, message: Message) | 当频道的消息被删除时 |
55 |
56 | - **注:需要引入`Message`**
57 |
58 | ```python
59 | from botpy.message import Message
60 | ```
61 |
62 | ```python
63 | class MyClient(botpy.Client):
64 | async def on_at_message_create(self, message: Message):
65 | """
66 | 此处为处理该事件的代码
67 | """
68 |
69 | async def on_public_message_delete(self, message: Message):
70 | """
71 | 此处为处理该事件的代码
72 | """
73 | ```
74 |
75 | ### 消息事件的监听
76 |
77 | - **仅 私域 机器人能够设置此 intents**
78 |
79 | 首先需要订阅事件`guild_messages`
80 |
81 | ```python
82 | intents = botpy.Intents(guild_messages=True)
83 | client = MyClient(intents=intents)
84 | ```
85 |
86 | | 对应函数 | 说明 |
87 | | ----------------------------------------- | -------------------------------------------------------------- |
88 | | on_message_create(self, message: Message) | 发送消息事件,代表频道内的全部消息,而不只是 at 机器人的消息。内容与 AT_MESSAGE_CREATE 相同 |
89 | | on_message_delete(self, message: Message) | 删除(撤回)消息事件 |
90 |
91 | - **注:需要引入`Message`**
92 |
93 | ```python
94 | from botpy.message import Message
95 | ```
96 |
97 | ```python
98 | class MyClient(botpy.Client):
99 | async def on_message_create(self, message: Message):
100 | """
101 | 此处为处理该事件的代码
102 | """
103 |
104 | async def on_message_delete(self, message: Message):
105 | """
106 | 此处为处理该事件的代码
107 | """
108 | ```
109 |
110 | ### 私信事件的监听
111 |
112 | 首先需要订阅事件`direct_message`
113 |
114 | ```python
115 | intents = botpy.Intents(direct_message=True)
116 | client = MyClient(intents=intents)
117 | ```
118 |
119 | | 对应函数 | 说明 |
120 | | ------------------------------------------------------ | ---------------- |
121 | | on_direct_message_create(self, message: DirectMessage) | 当收到用户发给机器人的私信消息时 |
122 | | on_direct_message_delete(self, message: DirectMessage) | 删除(撤回)消息事件 |
123 |
124 | - **注:需要引入`DirectMessage`**
125 |
126 | ```python
127 | from botpy.message import DirectMessage
128 | ```
129 |
130 | ```python
131 | class MyClient(botpy.Client):
132 | async def on_direct_message_create(self, message: DirectMessage):
133 | """
134 | 此处为处理该事件的代码
135 | """
136 |
137 | async def on_direct_message_delete(self, message: DirectMessage):
138 | """
139 | 此处为处理该事件的代码
140 | """
141 | ```
142 |
143 | ### 消息相关互动事件的监听
144 |
145 | 首先需要订阅事件`guild_message_reactions`
146 |
147 | ```python
148 | intents = botpy.Intents(guild_message_reactions=True)
149 | client = MyClient(intents=intents)
150 | ```
151 |
152 | | 对应函数 | 说明 |
153 | | ---------------------------------------------------- | --------- |
154 | | on_message_reaction_add(self, reaction: Reaction) | 为消息添加表情表态 |
155 | | on_message_reaction_remove(self, reaction: Reaction) | 为消息删除表情表态 |
156 |
157 | - **注:需要引入`Reaction`**
158 |
159 | ```python
160 | from botpy.reaction import Reaction
161 | ```
162 |
163 | ```python
164 | class MyClient(botpy.Client):
165 | async def on_message_reaction_add(self, reaction: Reaction):
166 | """
167 | 此处为处理该事件的代码
168 | """
169 |
170 | async def on_message_reaction_remove(self, reaction: Reaction):
171 | """
172 | 此处为处理该事件的代码
173 | """
174 | ```
175 |
176 | ### 频道事件的监听
177 |
178 | 首先需要订阅事件`guilds`
179 |
180 | ```python
181 | intents = botpy.Intents(guilds=True)
182 | client = MyClient(intents=intents)
183 | ```
184 |
185 | | 对应函数 | 说明 |
186 | | ----------------------------------------- | ------------- |
187 | | on_guild_create(self, guild: Guild) | 当机器人加入新guild时 |
188 | | on_guild_update(self, guild: Guild) | 当guild资料发生变更时 |
189 | | on_guild_delete(self, guild: Guild) | 当机器人退出guild时 |
190 | | on_channel_create(self, channel: Channel) | 当channel被创建时 |
191 | | on_channel_update(self, channel: Channel) | 当channel被更新时 |
192 | | on_channel_delete(self, channel: Channel) | 当channel被删除时 |
193 |
194 | - **注:需要引入`Guild`和`Channel`**
195 |
196 | ```python
197 | from botpy.guild import Guild
198 | from botpy.channel import Channel
199 | ```
200 |
201 | ```python
202 | class MyClient(botpy.Client):
203 | async def on_guild_create(self, guild: Guild):
204 | """
205 | 此处为处理该事件的代码
206 | """
207 |
208 | async def on_guild_update(self, guild: Guild):
209 | """
210 | 此处为处理该事件的代码
211 | """
212 |
213 | async def on_guild_delete(self, guild: Guild):
214 | """
215 | 此处为处理该事件的代码
216 | """
217 |
218 | async def on_channel_create(self, channel: Channel):
219 | """
220 | 此处为处理该事件的代码
221 | """
222 |
223 | async def on_channel_update(self, channel: Channel):
224 | """
225 | 此处为处理该事件的代码
226 | """
227 |
228 | async def on_channel_delete(self, channel: Channel):
229 | """
230 | 此处为处理该事件的代码
231 | """
232 | ```
233 |
234 | ### 频道成员事件的监听
235 |
236 | 首先需要订阅事件`guild_members`
237 |
238 | ```python
239 | intents = botpy.Intents(guild_members=True)
240 | client = MyClient(intents=intents)
241 | ```
242 |
243 | | 对应函数 | 说明 |
244 | | -------------------------------------------- | -------- |
245 | | on_guild_member_add(self, member: Member) | 当成员加入时 |
246 | | on_guild_member_update(self, member: Member) | 当成员资料变更时 |
247 | | on_guild_member_remove(self, member: Member) | 当成员被移除时 |
248 |
249 | - **注:需要引入`GuildMember`**
250 |
251 | ```python
252 | from botpy.user import Member
253 | ```
254 |
255 | ```python
256 | class MyClient(botpy.Client):
257 | async def on_guild_member_add(self, member: Member):
258 | """
259 | 此处为处理该事件的代码
260 | """
261 |
262 | async def on_guild_member_update(self, member: Member):
263 | """
264 | 此处为处理该事件的代码
265 | """
266 |
267 | async def on_guild_member_remove(self, member: Member):
268 | """
269 | 此处为处理该事件的代码
270 | """
271 | ```
272 |
273 | ### 互动事件的监听
274 |
275 | 首先需要订阅事件`interaction`
276 |
277 | ```python
278 | intents = botpy.Intents(interaction=True)
279 | client = MyClient(intents=intents)
280 | ```
281 |
282 | | 对应函数 | 说明 |
283 | | ----------------------------------------------------- | ---------------- |
284 | | on_interaction_create(self, interaction: Interaction) | 当收到用户发给机器人的私信消息时 |
285 |
286 | - **注:需要引入`Interaction`**
287 |
288 | ```python
289 | from botpy.interaction import Interaction
290 | ```
291 |
292 | ```python
293 | class MyClient(botpy.Client):
294 | async def on_interaction_create(self, interaction: Interaction):
295 | """
296 | 此处为处理该事件的代码
297 | """
298 | ```
299 |
300 | ### 消息审核事件的监听
301 |
302 | 首先需要订阅事件`message_audit`
303 |
304 | ```python
305 | intents = botpy.Intents(message_audit=True)
306 | client = MyClient(intents=intents)
307 | ```
308 |
309 | | 对应函数 | 说明 |
310 | | ---------------------------------------------------- | ------- |
311 | | on_message_audit_pass(self, message: MessageAudit) | 消息审核通过 |
312 | | on_message_audit_reject(self, message: MessageAudit) | 消息审核不通过 |
313 |
314 | - **注:需要引入`MessageAudit`**
315 |
316 | ```python
317 | from botpy.message import MessageAudit
318 | ```
319 |
320 | ```python
321 | class MyClient(botpy.Client):
322 | async def on_message_audit_pass(self, message: MessageAudit):
323 | """
324 | 此处为处理该事件的代码
325 | """
326 |
327 | async def on_message_audit_reject(self, message: MessageAudit):
328 | """
329 | 此处为处理该事件的代码
330 | """
331 | ```
332 |
333 | ### 论坛事件的监听
334 |
335 | - **仅 私域 机器人能够设置此 intents**
336 |
337 | 首先需要订阅事件`forums`
338 |
339 | ```python
340 | intents = botpy.Intents(forums=True)
341 | client = MyClient(intents=intents)
342 | ```
343 |
344 | | 对应函数 | 说明 |
345 | | ------------------------------------------------------------- | ---------- |
346 | | on_forum_thread_create(self, thread: Thread) | 当用户创建主题时 |
347 | | on_forum_thread_update(self, thread: Thread) | 当用户更新主题时 |
348 | | on_forum_thread_delete(self, thread: Thread) | 当用户删除主题时 |
349 | | on_forum_post_create(self, post: Post) | 当用户创建帖子时 |
350 | | on_forum_post_delete(self, post: Post) | 当用户删除帖子时 |
351 | | on_forum_reply_create(self, reply: Reply) | 当用户回复评论时 |
352 | | on_forum_reply_delete(self, reply: Reply) | 当用户删除评论时 |
353 | | on_forum_publish_audit_result(self, auditresult: AuditResult) | 当用户发表审核通过时 |
354 |
355 | - **注:需要引入`Thread`、`Post`、`Reply`和`AuditResult`**
356 |
357 | ```python
358 | from botpy.forum import Thread
359 | from botpy.types.forum import Post, Reply, AuditResult
360 | ```
361 |
362 | ```python
363 | class MyClient(botpy.Client):
364 | async def on_forum_thread_create(self, thread: Thread):
365 | """
366 | 此处为处理该事件的代码
367 | """
368 |
369 | async def on_forum_thread_update(self, thread: Thread):
370 | """
371 | 此处为处理该事件的代码
372 | """
373 |
374 | async def on_forum_thread_delete(self, thread: Thread):
375 | """
376 | 此处为处理该事件的代码
377 | """
378 |
379 | async def on_forum_post_create(self, post: Post):
380 | """
381 | 此处为处理该事件的代码
382 | """
383 |
384 | async def on_forum_post_delete(self, post: Post):
385 | """
386 | 此处为处理该事件的代码
387 | """
388 |
389 | async def on_forum_reply_create(self, reply: Reply):
390 | """
391 | 此处为处理该事件的代码
392 | """
393 |
394 | async def on_forum_reply_delete(self, reply: Reply):
395 | """
396 | 此处为处理该事件的代码
397 | """
398 |
399 | async def on_forum_publish_audit_result(self, auditresult: AuditResult):
400 | """
401 | 此处为处理该事件的代码
402 | """
403 | ```
404 |
405 | ### 音频事件的监听
406 |
407 | 首先需要订阅事件`audio_action`
408 |
409 | ```python
410 | intents = botpy.Intents(audio_action=True)
411 | client = MyClient(intents=intents)
412 | ```
413 |
414 | | 对应函数 | 说明 |
415 | |--------------------------------------|---------|
416 | | on_audio_start(self, audio: Audio) | 音频开始播放时 |
417 | | on_audio_finish(self, audio: Audio) | 音频播放结束时 |
418 | | on_audio_on_mic(self, audio: Audio) | 上麦时 |
419 | | on_audio_off_mic(self, audio: Audio) | 下麦时 |
420 |
421 | - **注:需要引入`Audio`**
422 |
423 | ```python
424 | from botpy.audio import Audio
425 | ```
426 |
427 | ```python
428 | class MyClient(botpy.Client):
429 | async def on_audio_start(self, audio: Audio):
430 | """
431 | 此处为处理该事件的代码
432 | """
433 |
434 | async def on_audio_finish(self, audio: Audio):
435 | """
436 | 此处为处理该事件的代码
437 | """
438 |
439 | async def on_audio_on_mic(self, audio: Audio):
440 | """
441 | 此处为处理该事件的代码
442 | """
443 |
444 | async def on_audio_off_mic(self, audio: Audio):
445 | """
446 | 此处为处理该事件的代码
447 | """
448 | ```
449 |
450 | ### 音视频/直播子频道成员进出事件
451 |
452 | 首先需要订阅事件`audio_or_live_channel_member`
453 |
454 | ```python
455 | intents = botpy.Intents(audio_or_live_channel_member=True)
456 | client = MyClient(intents=intents)
457 | ```
458 |
459 | | 对应函数 | 说明 |
460 | |------------------------------------------------------------------------|----------------|
461 | | on_audio_or_live_channel_member_enter(self, Public_Audio: PublicAudio) | 用户进入音视频/直播子频道时 |
462 | | on_audio_or_live_channel_member_exit(self, Public_Audio: PublicAudio) | 用户退出音视频/直播子频道时 |
463 |
464 | - **注:需要引入`PublicAudio`**
465 |
466 | ```python
467 | from botpy.audio import PublicAudio
468 | ```
469 |
470 | ```python
471 | class MyClient(botpy.Client):
472 | async def on_audio_or_live_channel_member_enter(self, Public_Audio: PublicAudio):
473 | """
474 | 此处为处理该事件的代码
475 | """
476 |
477 | async def on_audio_or_live_channel_member_exit(self, Public_Audio: PublicAudio):
478 | """
479 | 此处为处理该事件的代码
480 | """
481 | ```
482 |
483 | ### 开放论坛事件对象
484 |
485 | 首先需要订阅事件`open_forum_event`
486 |
487 | ```python
488 | intents = botpy.Intents(open_forum_event=True)
489 | client = MyClient(intents=intents)
490 | ```
491 |
492 | | 对应函数 | 说明 |
493 | |------------------------------------------------------------------|---------|
494 | | on_open_forum_thread_create(self, open_forum_thread: OpenThread) | 用户创建主题时 |
495 | | on_open_forum_thread_update(self, open_forum_thread: OpenThread) | 用户修改主题时 |
496 | | on_open_forum_thread_delete(self, open_forum_thread: OpenThread) | 用户删除主题时 |
497 | | on_open_forum_post_create(self, open_forum_thread: OpenThread) | 用户创建帖子时 |
498 | | on_open_forum_post_delete(self, open_forum_thread: OpenThread) | 用户删除帖子时 |
499 | | on_open_forum_reply_create(self, open_forum_thread: OpenThread) | 用户回复评论时 |
500 | | on_open_forum_reply_delete(self, open_forum_thread: OpenThread) | 用户删除评论时 |
501 |
502 | - **注:需要引入`Audio`**
503 |
504 | ```python
505 | from botpy.audio import Audio
506 | ```
507 |
508 | ```python
509 | class MyClient(botpy.Client):
510 | async def on_audio_start(self, audio: Audio):
511 | """
512 | 此处为处理该事件的代码
513 | """
514 |
515 | async def on_audio_finish(self, audio: Audio):
516 | """
517 | 此处为处理该事件的代码
518 | """
519 |
520 | async def on_audio_on_mic(self, audio: Audio):
521 | """
522 | 此处为处理该事件的代码
523 | """
524 |
525 | async def on_audio_off_mic(self, audio: Audio):
526 | """
527 | 此处为处理该事件的代码
528 | """
529 | ```
530 |
531 | ## 订阅事件的方法
532 |
533 | ### 方法一:
534 |
535 | ```python
536 | intents = botpy.Intents()
537 | client = MyClient(intents=intents)
538 | ```
539 |
540 | 在Intents中填入对应的[参数](#参数列表)
541 |
542 | 例子:
543 |
544 | ```python
545 | intents = botpy.Intents(public_guild_messages=True, direct_message=True, guilds=True)
546 | ```
547 |
548 | ### 方法二:
549 |
550 | ```python
551 | intents = botpy.Intents.none()
552 | ```
553 |
554 | 然后打开对应的订阅([参数列表](#参数列表))
555 |
556 | ```python
557 | intents.public_guild_messages = True
558 | intents.direct_message = True
559 | intents.guilds = True
560 | ```
561 |
562 | - **说明**
563 |
564 | 方法二对应的快捷订阅方式为
565 |
566 | 1. 订阅所有事件
567 |
568 | ```python
569 | intents = botpy.Intents.all()
570 | ```
571 |
572 | 2. 订阅所有的公域事件
573 |
574 | ```python
575 | intents = botpy.Intents.default()
576 | ```
577 |
578 | #### 参数列表
579 |
580 | | 参数 | 含义 |
581 | | ----------------------- | ---------------------------------- |
582 | | public_guild_messages | 公域消息事件 |
583 | | guild_messages | 消息事件 **(仅 `私域` 机器人能够设置此 intents)** |
584 | | direct_message | 私信事件 |
585 | | guild_message_reactions | 消息相关互动事件 |
586 | | guilds | 频道事件 |
587 | | guild_members | 频道成员事件 |
588 | | interaction | 互动事件 |
589 | | message_audit | 消息审核事件 |
590 | | forums | 论坛事件 **(仅 `私域` 机器人能够设置此 intents)** |
591 | | audio_action | 音频事件 |
592 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # examples
2 |
3 | 该目录用于存放基于 botpy 开发的机器人的完整示例。
4 |
5 | ```
6 | examples/
7 | .
8 | ├── README.md
9 | ├── config.example.yaml # 示例配置文件(需要修改为config.yaml)
10 | ├── demo_announce.py # 机器人公告API使用示例
11 | ├── demo_api_permission.py # 机器人授权查询API使用示例
12 | ├── demo_at_reply.py # 机器人at被动回复async示例
13 | ├── demo_at_reply_ark.py # 机器人at被动回复ark消息示例
14 | ├── demo_at_reply_embed.py # 机器人at被动回复embed消息示例
15 | ├── demo_at_reply_command.py # 机器人at被动使用Command指令装饰器回复消息示例
16 | ├── demo_at_reply_file_data.py # 机器人at被动回复本地图片消息示例
17 | ├── demo_at_reply_keyboard.py # 机器人at被动回复md带内嵌键盘的示例
18 | ├── demo_at_reply_markdown.py # 机器人at被动回复md消息示例
19 | ├── demo_at_reply_reference.py # 机器人at被动回复消息引用示例
20 | ├── demo_dms_reply.py # 机器人私信被动回复示例
21 | ├── demo_get_reaction_users.py # 机器人获取表情表态成员列表示例
22 | ├── demo_guild_member_event.py # 机器人频道成员变化事件示例
23 | ├── demo_interaction.py # 机器人互动事件示例(未启用)
24 | ├── demo_pins_message.py # 机器人消息置顶示例
25 | ├── demo_recall.py # 机器人消息撤回示例
26 | ├── demo_schedule.py # 机器人日程相关示例
27 | ├── demo_group_reply_text.py # 机器人群内发消息相关示例
28 | ├── demo_group_reply_file.py # 机器人群内发富媒体消息相关示例
29 | ├── demo_group_manage_event.py # 机器人群管理事件
30 | ├── demo_c2c_reply_text.py # 机器人好友内发消息相关示例
31 | ├── demo_c2c_reply_file.py # 机器人好友内发富媒体消息相关示例
32 | ├── demo_c2c_manage_event.py # 机器人好友管理事件
33 | ├── demo_audio_or_live_channel_member.py # 音视频/直播子频道成员进出事件
34 | ├── demo_open_forum_event.py # 开放论坛事件对象
35 | ```
36 |
37 | ## 环境安装
38 |
39 | ``` bash
40 | pip install qq-botpy
41 | ```
42 |
43 | ## 使用方法
44 |
45 | 1. 拷贝 config.example.yaml 为 config.yaml :
46 |
47 | ``` bash
48 | cp config.example.yaml config.yaml
49 | ```
50 |
51 | 2. 修改 config.yaml ,填入自己的 BotAppID 和 Bot secret 。
52 | 3. 运行机器人。例如:
53 |
54 | ``` bash
55 | python3 demo_at_reply.py
56 | ```
57 |
--------------------------------------------------------------------------------
/examples/config.example.yaml:
--------------------------------------------------------------------------------
1 | appid: "123"
2 | secret: "xxxx"
--------------------------------------------------------------------------------
/examples/demo_announce.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.message import Message
8 | from botpy.types.announce import AnnouncesType
9 | from botpy.ext.cog_yaml import read
10 |
11 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
12 |
13 | _log = logging.get_logger()
14 |
15 |
16 | class MyClient(botpy.Client):
17 | async def on_at_message_create(self, message: Message):
18 | _log.info(f"{self.robot.name}receive message {message.content}")
19 |
20 | # 先发送消息告知用户
21 | await self.api.post_message(message.channel_id, content="command received: %s" % message.content)
22 |
23 | # 输入/xxx后的处理
24 | # 对用户引用回复的消息设置/删除公告
25 | message_id = message.message_reference.message_id
26 | if "/建公告" in message.content:
27 | await self.api.create_announce(message.guild_id, message.channel_id, message_id)
28 |
29 | elif "/删公告" in message.content:
30 | await self.api.delete_announce(message.guild_id, message_id)
31 |
32 | elif "/设置推荐子频道" in message.content:
33 | channel_list = [{"channel_id": message.channel_id, "introduce": "introduce"}]
34 | await self.api.create_recommend_announce(message.guild_id, AnnouncesType.MEMBER, channel_list)
35 |
36 |
37 | if __name__ == "__main__":
38 | # 通过预设置的类型,设置需要监听的事件通道
39 | # intents = botpy.Intents.none()
40 | # intents.public_guild_messages=True
41 |
42 | # 通过kwargs,设置需要监听的事件通道
43 | intents = botpy.Intents(public_guild_messages=True)
44 | client = MyClient(intents=intents)
45 | client.run(appid=test_config["appid"], secret=test_config["secret"])
46 |
--------------------------------------------------------------------------------
/examples/demo_api_permission.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.message import Message
8 | from botpy.types.permission import APIPermissionDemandIdentify
9 | from botpy.ext.cog_yaml import read
10 |
11 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
12 |
13 | _log = logging.get_logger()
14 |
15 |
16 | class MyClient(botpy.Client):
17 | async def on_ready(self):
18 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
19 |
20 | async def on_at_message_create(self, message: Message):
21 | # 先发送消息告知用户
22 | await message.reply(content=f"机器人{self.robot.name}创建日程{message.content}")
23 |
24 | # 输入/xxx后的处理
25 | if "/权限列表" in message.content:
26 | apis = await self.api.get_permissions(message.guild_id)
27 | for api in apis:
28 | _log.info("api: %s" % api["desc"] + ", status: %d" % api["auth_status"])
29 | if "/请求权限" in message.content:
30 | demand_identity = APIPermissionDemandIdentify(path="/guilds/{guild_id}/members/{user_id}", method="GET")
31 | demand = await self.api.post_permission_demand(
32 | message.guild_id, message.channel_id, demand_identity, "获取当前频道成员信息"
33 | )
34 | _log.info("api title: %s" % demand["title"] + ", desc: %s" % demand["desc"])
35 |
36 |
37 | if __name__ == "__main__":
38 | # 通过预设置的类型,设置需要监听的事件通道
39 | # intents = botpy.Intents.none()
40 | # intents.public_guild_messages=True
41 |
42 | # 通过kwargs,设置需要监听的事件通道
43 | intents = botpy.Intents(public_guild_messages=True)
44 | client = MyClient(intents=intents)
45 | client.run(appid=test_config["appid"], secret=test_config["secret"])
46 |
--------------------------------------------------------------------------------
/examples/demo_at_reply.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import asyncio
3 | import os
4 |
5 | import botpy
6 | from botpy import logging
7 | from botpy.ext.cog_yaml import read
8 | from botpy.message import Message
9 |
10 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
11 |
12 | _log = logging.get_logger()
13 |
14 |
15 | class MyClient(botpy.Client):
16 | async def on_ready(self):
17 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
18 |
19 | async def on_at_message_create(self, message: Message):
20 | _log.info(message.author.avatar)
21 | if "sleep" in message.content:
22 | await asyncio.sleep(10)
23 | _log.info(message.author.username)
24 | await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}")
25 |
26 |
27 | if __name__ == "__main__":
28 | # 通过预设置的类型,设置需要监听的事件通道
29 | # intents = botpy.Intents.none()
30 | # intents.public_guild_messages=True
31 |
32 | # 通过kwargs,设置需要监听的事件通道
33 | intents = botpy.Intents(public_guild_messages=True)
34 | client = MyClient(intents=intents)
35 | client.run(appid=test_config["appid"], secret=test_config["secret"])
36 |
--------------------------------------------------------------------------------
/examples/demo_at_reply_ark.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.message import Message
8 | from botpy.types.message import Ark, ArkKv
9 | from botpy.ext.cog_yaml import read
10 |
11 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
12 |
13 | _log = logging.get_logger()
14 |
15 |
16 | class MyClient(botpy.Client):
17 | async def on_ready(self):
18 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
19 |
20 | async def on_at_message_create(self, message: Message):
21 | # 两种方式构造消息发送请求数据对象
22 | payload: Ark = Ark(
23 | template_id=37,
24 | kv=[
25 | ArkKv(key="#METATITLE#", value="通知提醒"),
26 | ArkKv(key="#PROMPT#", value="标题"),
27 | ArkKv(key="#TITLE#", value="标题"),
28 | ArkKv(key="#METACOVER#", value="https://vfiles.gtimg.cn/vupload/20211029/bf0ed01635493790634.jpg"),
29 | ],
30 | )
31 | # payload = {
32 | # "template_id": 37,
33 | # "kv": [
34 | # {"key": "#METATITLE#", "value": "通知提醒"},
35 | # {"key": "#PROMPT#", "value": "标题"},
36 | # {"key": "#TITLE#", "value": "标题"},
37 | # {"key": "#METACOVER#", "value": "https://vfiles.gtimg.cn/vupload/20211029/bf0ed01635493790634.jpg"},
38 | # ],
39 | # }
40 |
41 | await self.api.post_message(channel_id=message.channel_id, ark=payload)
42 | # await message.reply(ark=payload) # 这样也可以
43 |
44 |
45 | if __name__ == "__main__":
46 | # 通过预设置的类型,设置需要监听的事件通道
47 | # intents = botpy.Intents.none()
48 | # intents.public_guild_messages=True
49 |
50 | # 通过kwargs,设置需要监听的事件通道
51 | intents = botpy.Intents(public_guild_messages=True)
52 | client = MyClient(intents=intents)
53 | client.run(appid=test_config["appid"], secret=test_config["secret"])
54 |
--------------------------------------------------------------------------------
/examples/demo_at_reply_command.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging, BotAPI
6 |
7 | from botpy.ext.command_util import Commands
8 | from botpy.message import Message
9 | from botpy.ext.cog_yaml import read
10 |
11 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
12 |
13 | _log = logging.get_logger()
14 |
15 |
16 | @Commands("你好", "hello")
17 | async def hello(api: BotAPI, message: Message, params=None):
18 | _log.info(params)
19 | # 第一种用reply发送消息
20 | await message.reply(content=params)
21 | # 第二种用api.post_message发送消息
22 | await api.post_message(channel_id=message.channel_id, content=params, msg_id=message.id)
23 | return True
24 |
25 |
26 | @Commands("晚安")
27 | async def good_night(api: BotAPI, message: Message, params=None):
28 | _log.info(params)
29 | # 第一种用reply发送消息
30 | await message.reply(content=params)
31 | # 第二种用api.post_message发送消息
32 | await api.post_message(channel_id=message.channel_id, content=params, msg_id=message.id)
33 | return True
34 |
35 |
36 | class MyClient(botpy.Client):
37 | async def on_at_message_create(self, message: Message):
38 | # 注册指令handler
39 | handlers = [
40 | hello,
41 | good_night,
42 | ]
43 | for handler in handlers:
44 | if await handler(api=self.api, message=message):
45 | return
46 |
47 |
48 | if __name__ == "__main__":
49 | # 通过预设置的类型,设置需要监听的事件通道
50 | # intents = botpy.Intents.none()
51 | # intents.public_guild_messages=True
52 |
53 | # 通过kwargs,设置需要监听的事件通道
54 | intents = botpy.Intents(public_guild_messages=True)
55 | client = MyClient(intents=intents)
56 | client.run(appid=test_config["appid"], secret=test_config["secret"])
57 |
--------------------------------------------------------------------------------
/examples/demo_at_reply_embed.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.message import Message
8 | from botpy.types.message import Embed, EmbedField
9 | from botpy.ext.cog_yaml import read
10 |
11 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
12 |
13 | _log = logging.get_logger()
14 |
15 |
16 | class MyClient(botpy.Client):
17 | async def on_ready(self):
18 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
19 |
20 | async def on_at_message_create(self, message: Message):
21 | # 构造消息发送请求数据对象
22 | embed = Embed(
23 | title="embed消息",
24 | prompt="消息透传显示",
25 | fields=[
26 | EmbedField(name="<@!1234>hello world"),
27 | EmbedField(name="<@!1234>hello world"),
28 | ],
29 | )
30 |
31 | # embed = {
32 | # "title": "embed消息",
33 | # "prompt": "消息透传显示",
34 | # "fields": [
35 | # {"name": "<@!1234>hello world"},
36 | # {"name": "<@!1234>hello world"},
37 | # ],
38 | # }
39 |
40 | await self.api.post_message(channel_id=message.channel_id, embed=embed)
41 | # await message.reply(embed=embed) # 这样也可以
42 |
43 |
44 | if __name__ == "__main__":
45 | # 通过预设置的类型,设置需要监听的事件通道
46 | # intents = botpy.Intents.none()
47 | # intents.public_guild_messages=True
48 |
49 | # 通过kwargs,设置需要监听的事件通道
50 | intents = botpy.Intents(public_guild_messages=True)
51 | client = MyClient(intents=intents)
52 | client.run(appid=test_config["appid"], secret=test_config["secret"])
53 |
--------------------------------------------------------------------------------
/examples/demo_at_reply_file_data.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.message import Message
8 | from botpy.ext.cog_yaml import read
9 |
10 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
11 |
12 | _log = logging.get_logger()
13 |
14 |
15 | class MyClient(botpy.Client):
16 | async def on_ready(self):
17 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
18 |
19 | async def on_at_message_create(self, message: Message):
20 | # 方法1(阅读档案后传入bytes类型图片数据):
21 | with open("resource/test.png", "rb") as img:
22 | img_bytes = img.read()
23 | await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}", file_image=img_bytes)
24 | # 方法2(打开档案后直接传入档案):
25 | with open("resource/test.png", "rb") as img:
26 | await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}", file_image=img)
27 | # 方法3(直接传入图片路径):
28 | await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}", file_image="resource/test.png")
29 |
30 |
31 | if __name__ == "__main__":
32 | # 通过预设置的类型,设置需要监听的事件通道
33 | # intents = botpy.Intents.none()
34 | # intents.public_guild_messages=True
35 |
36 | # 通过kwargs,设置需要监听的事件通道
37 | intents = botpy.Intents(public_guild_messages=True)
38 | client = MyClient(intents=intents)
39 | client.run(appid=test_config["appid"], secret=test_config["secret"])
40 |
--------------------------------------------------------------------------------
/examples/demo_at_reply_keyboard.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import BotAPI
6 |
7 | from botpy.message import Message
8 | from botpy.types.inline import Keyboard, Button, RenderData, Action, Permission, KeyboardRow
9 | from botpy.types.message import MarkdownPayload, KeyboardPayload
10 | from botpy.ext.cog_yaml import read
11 |
12 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
13 |
14 |
15 | class MyClient(botpy.Client):
16 | async def on_at_message_create(self, message: Message):
17 | await send_template_keyboard(self.api, message)
18 | await send_self_defined_keyboard(self.api, message)
19 |
20 |
21 | async def send_template_keyboard(api: BotAPI, message: Message):
22 | markdown = MarkdownPayload(content="# 123 \n 今天是个好天气")
23 | keyboard = KeyboardPayload(id="62")
24 | await api.post_keyboard_message(message.channel_id, markdown=markdown, keyboard=keyboard)
25 |
26 |
27 | async def send_self_defined_keyboard(api: BotAPI, message: Message):
28 | markdown = MarkdownPayload(content="# 标题 \n## 简介 \n内容")
29 | keyboard = KeyboardPayload(content=build_a_demo_keyboard())
30 | await api.post_keyboard_message(message.channel_id, markdown=markdown, keyboard=keyboard)
31 |
32 |
33 | def build_a_demo_keyboard() -> Keyboard:
34 | """
35 | 创建一个只有一行且该行只有一个 button 的键盘
36 | """
37 | button1 = Button(
38 | id="1",
39 | render_data=RenderData(label="button", visited_label="BUTTON", style=0),
40 | action=Action(
41 | type=2,
42 | permission=Permission(type=2, specify_role_ids=["1"], specify_user_ids=["1"]),
43 | click_limit=10,
44 | data="/搜索",
45 | at_bot_show_channel_list=True,
46 | ),
47 | )
48 |
49 | row1 = KeyboardRow(buttons=[button1])
50 | return Keyboard(rows=[row1])
51 |
52 |
53 | if __name__ == "__main__":
54 | # async的异步接口的使用示例
55 | intents = botpy.Intents(public_guild_messages=True)
56 | client = MyClient(intents=intents)
57 | client.run(appid=test_config["appid"], secret=test_config["secret"])
58 |
--------------------------------------------------------------------------------
/examples/demo_at_reply_markdown.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.message import Message
8 | from botpy.types.message import MarkdownPayload, MessageMarkdownParams
9 | from botpy.ext.cog_yaml import read
10 |
11 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
12 |
13 | _log = logging.get_logger()
14 |
15 |
16 | class MyClient(botpy.Client):
17 | async def on_ready(self):
18 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
19 |
20 | async def handle_send_markdown_by_template(self, channel_id, msg_id):
21 | params = [
22 | MessageMarkdownParams(key="title", values=["标题"]),
23 | MessageMarkdownParams(key="content", values=["为了成为一名合格的巫师,请务必阅读频道公告", "藏馆黑色魔法书"]),
24 | ]
25 | markdown = MarkdownPayload(custom_template_id="65", params=params)
26 |
27 | # 通过api发送回复消息
28 | await self.api.post_message(channel_id, markdown=markdown, msg_id=msg_id)
29 |
30 | async def handle_send_markdown_by_content(self, channel_id, msg_id):
31 | markdown = MarkdownPayload(content="# 标题 \n## 简介很开心 \n内容")
32 | # 通过api发送回复消息
33 | await self.api.post_message(channel_id, markdown=markdown, msg_id=msg_id)
34 |
35 | async def on_at_message_create(self, message: Message):
36 | await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}")
37 | await self.handle_send_markdown_by_template(message.channel_id, message.id)
38 | await self.handle_send_markdown_by_content(message.channel_id, message.id)
39 |
40 |
41 | if __name__ == "__main__":
42 | # 通过预设置的类型,设置需要监听的事件通道
43 | # intents = botpy.Intents.none()
44 | # intents.public_guild_messages=True
45 |
46 | # 通过kwargs,设置需要监听的事件通道
47 | intents = botpy.Intents(public_guild_messages=True)
48 | client = MyClient(intents=intents)
49 | client.run(appid=test_config["appid"], secret=test_config["secret"])
50 |
--------------------------------------------------------------------------------
/examples/demo_at_reply_reference.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.types.message import Reference
8 | from botpy.message import Message
9 | from botpy.ext.cog_yaml import read
10 |
11 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
12 |
13 | _log = logging.get_logger()
14 |
15 |
16 | class MyClient(botpy.Client):
17 | async def on_ready(self):
18 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
19 |
20 | async def on_at_message_create(self, message: Message):
21 | # 构造消息发送请求数据对象
22 | message_reference = Reference(message_id=message.id)
23 | # 通过api发送回复消息
24 | await self.api.post_message(
25 | channel_id=message.channel_id,
26 | content="这是一条引用消息",
27 | msg_id=message.id,
28 | message_reference=message_reference,
29 | )
30 |
31 |
32 | if __name__ == "__main__":
33 | # 通过预设置的类型,设置需要监听的事件通道
34 | # intents = botpy.Intents.none()
35 | # intents.public_guild_messages=True
36 |
37 | # 通过kwargs,设置需要监听的事件通道
38 | intents = botpy.Intents(public_guild_messages=True)
39 | client = MyClient(intents=intents)
40 | client.run(appid=test_config["appid"], secret=test_config["secret"])
41 |
--------------------------------------------------------------------------------
/examples/demo_audio_or_live_channel_member.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import botpy
3 |
4 | import os
5 | from botpy import logging
6 | from botpy.audio import PublicAudio
7 | from botpy.ext.cog_yaml import read
8 |
9 | _log = logging.get_logger()
10 |
11 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
12 |
13 | class MyClient(botpy.Client):
14 | async def on_ready(self):
15 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
16 |
17 | async def on_audio_or_live_channel_member_enter(self, Public_Audio: PublicAudio):
18 | if Public_Audio.channel_type == 2:
19 | _log.info("%s 加入了音视频子频道" % Public_Audio.user_id)
20 | elif Public_Audio.channel_type == 5:
21 | _log.info("%s 加入了直播子频道" % Public_Audio.user_id)
22 |
23 | async def on_audio_or_live_channel_member_exit(self, Public_Audio: PublicAudio):
24 | if Public_Audio.channel_type == 2:
25 | _log.info("%s 退出了音视频子频道" % Public_Audio.user_id)
26 | elif Public_Audio.channel_type == 5:
27 | _log.info("%s 退出了直播子频道" % Public_Audio.user_id)
28 |
29 |
30 | if __name__ == "__main__":
31 | # 通过预设置的类型,设置需要监听的事件通道
32 | # intents = botpy.Intents.none()
33 | # intents.public_guild_messages=True
34 |
35 | # 通过kwargs,设置需要监听的事件通道
36 | intents = botpy.Intents(audio_or_live_channel_member=True)
37 | client = MyClient(intents=intents)
38 | client.run(appid=test_config["appid"], secret=test_config["secret"])
39 |
--------------------------------------------------------------------------------
/examples/demo_c2c_manage_event.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 | from botpy.ext.cog_yaml import read
7 | from botpy.manage import C2CManageEvent
8 |
9 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
10 |
11 | _log = logging.get_logger()
12 |
13 |
14 | class MyClient(botpy.Client):
15 | async def on_friend_add(self, event: C2CManageEvent):
16 | _log.info("用户添加机器人:" + str(event))
17 | await self.api.post_c2c_message(
18 | openid=event.openid,
19 | msg_type=0,
20 | event_id=event.event_id,
21 | content="hello",
22 | )
23 |
24 | async def on_friend_del(self, event: C2CManageEvent):
25 | _log.info("用户删除机器人:" + str(event))
26 |
27 | async def on_c2c_msg_reject(self, event: C2CManageEvent):
28 | _log.info("用户关闭机器人主动消息:" + str(event))
29 |
30 | async def on_c2c_msg_receive(self, event: C2CManageEvent):
31 | _log.info("用户打开机器人主动消息:" + str(event))
32 |
33 |
34 | if __name__ == "__main__":
35 | # 通过预设置的类型,设置需要监听的事件通道
36 | # intents = botpy.Intents.none()
37 | # intents.public_messages=True
38 |
39 | # 通过kwargs,设置需要监听的事件通道
40 | intents = botpy.Intents(public_messages=True)
41 | client = MyClient(intents=intents)
42 | client.run(appid=test_config["appid"], secret=test_config["secret"])
43 |
--------------------------------------------------------------------------------
/examples/demo_c2c_reply_file.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import asyncio
3 | import os
4 |
5 | import botpy
6 | from botpy import logging
7 | from botpy.ext.cog_yaml import read
8 | from botpy.message import C2CMessage
9 |
10 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
11 |
12 | _log = logging.get_logger()
13 |
14 | class MyClient(botpy.Client):
15 | async def on_ready(self):
16 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
17 |
18 | async def on_c2c_message_create(self, message: C2CMessage):
19 | file_url = "" # 这里需要填写上传的资源Url
20 | uploadMedia = await message._api.post_c2c_file(
21 | openid=message.author.user_openid,
22 | file_type=1, # 文件类型要对应上,具体支持的类型见方法说明
23 | url=file_url # 文件Url
24 | )
25 |
26 | # 资源上传后,会得到Media,用于发送消息
27 | await message._api.post_c2c_message(
28 | openid=message.author.user_openid,
29 | msg_type=7, # 7表示富媒体类型
30 | msg_id=message.id,
31 | media=uploadMedia
32 | )
33 |
34 |
35 | if __name__ == "__main__":
36 | # 通过预设置的类型,设置需要监听的事件通道
37 | # intents = botpy.Intents.none()
38 | # intents.public_messages=True
39 |
40 | # 通过kwargs,设置需要监听的事件通道
41 | intents = botpy.Intents(public_messages=True)
42 | client = MyClient(intents=intents)
43 | client.run(appid=test_config["appid"], secret=test_config["secret"])
--------------------------------------------------------------------------------
/examples/demo_c2c_reply_text.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import asyncio
3 | import os
4 |
5 | import botpy
6 | from botpy import logging
7 | from botpy.ext.cog_yaml import read
8 | from botpy.message import C2CMessage
9 |
10 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
11 |
12 | _log = logging.get_logger()
13 |
14 | class MyClient(botpy.Client):
15 | async def on_ready(self):
16 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
17 |
18 | async def on_c2c_message_create(self, message: C2CMessage):
19 | await message._api.post_c2c_message(
20 | openid=message.author.user_openid,
21 | msg_type=0, msg_id=message.id,
22 | content=f"我收到了你的消息:{message.content}"
23 | )
24 |
25 |
26 | if __name__ == "__main__":
27 | # 通过预设置的类型,设置需要监听的事件通道
28 | # intents = botpy.Intents.none()
29 | # intents.public_messages=True
30 |
31 | # 通过kwargs,设置需要监听的事件通道
32 | intents = botpy.Intents(public_messages=True)
33 | client = MyClient(intents=intents)
34 | client.run(appid=test_config["appid"], secret=test_config["secret"])
--------------------------------------------------------------------------------
/examples/demo_dms_reply.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.message import DirectMessage, Message
8 | from botpy.ext.cog_yaml import read
9 |
10 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
11 |
12 | _log = logging.get_logger()
13 |
14 |
15 | class MyClient(botpy.Client):
16 | async def on_ready(self):
17 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
18 |
19 | async def on_direct_message_create(self, message: DirectMessage):
20 | await self.api.post_dms(
21 | guild_id=message.guild_id,
22 | content=f"机器人{self.robot.name}收到你的私信了: {message.content}",
23 | msg_id=message.id,
24 | )
25 |
26 | async def on_at_message_create(self, message: Message):
27 | if "/私信" in message.content:
28 | dms_payload = await self.api.create_dms(message.guild_id, message.author.id)
29 | _log.info("发送私信")
30 | await self.api.post_dms(dms_payload["guild_id"], content="hello", msg_id=message.id)
31 |
32 |
33 | if __name__ == "__main__":
34 | # 通过预设置的类型,设置需要监听的事件通道
35 | # intents = botpy.Intents.none()
36 | # intents.public_guild_messages=True
37 |
38 | # 通过kwargs,设置需要监听的事件通道
39 | intents = botpy.Intents(direct_message=True, public_guild_messages=True)
40 | client = MyClient(intents=intents)
41 | client.run(appid=test_config["appid"], secret=test_config["secret"])
42 |
--------------------------------------------------------------------------------
/examples/demo_get_reaction_users.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | from typing import List
4 |
5 | import botpy
6 |
7 | from botpy.message import Message
8 | from botpy.types import reaction
9 | from botpy.types.user import User
10 | from botpy.ext.cog_yaml import read
11 |
12 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
13 |
14 |
15 | class MyClient(botpy.Client):
16 | async def on_at_message_create(self, message: Message):
17 | users: List[User] = []
18 | cookie = ""
19 | while True:
20 | reactionUsers: reaction.ReactionUsers = await self.api.get_reaction_users(
21 | "2568610",
22 | "088de19cbeb883e7e97110a2e39c0138d80d48acfc879406",
23 | 1,
24 | "4",
25 | cookie=cookie,
26 | )
27 |
28 | if not reactionUsers:
29 | break
30 |
31 | users.extend(reactionUsers["users"])
32 |
33 | if reactionUsers["is_end"]:
34 | break
35 | else:
36 | cookie = reactionUsers["cookie"]
37 |
38 | print(len(users))
39 | for user in users:
40 | print(user["username"])
41 |
42 |
43 | if __name__ == "__main__":
44 | # 通过预设置的类型,设置需要监听的事件通道
45 | # intents = botpy.Intents.none()
46 | # intents.public_guild_messages=True
47 | # 通过kwargs,设置需要监听的事件通道
48 | intents = botpy.Intents(public_guild_messages=True)
49 | client = MyClient(intents)
50 | client.run(appid=test_config["appid"], secret=test_config["secret"])
51 |
--------------------------------------------------------------------------------
/examples/demo_group_manage_event.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 | from botpy.ext.cog_yaml import read
7 | from botpy.manage import GroupManageEvent
8 |
9 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
10 |
11 | _log = logging.get_logger()
12 |
13 |
14 | class MyClient(botpy.Client):
15 | async def on_group_add_robot(self, event: GroupManageEvent):
16 | _log.info("机器人被添加到群聊:" + str(event))
17 | await self.api.post_group_message(
18 | group_openid=event.group_openid,
19 | msg_type=0,
20 | event_id=event.event_id,
21 | content="hello",
22 | )
23 |
24 | async def on_group_del_robot(self, event: GroupManageEvent):
25 | _log.info("机器人被移除群聊:" + str(event))
26 |
27 | async def on_group_msg_reject(self, event: GroupManageEvent):
28 | _log.info("群聊关闭机器人主动消息:" + str(event))
29 |
30 | async def on_group_msg_receive(self, event: GroupManageEvent):
31 | _log.info("群聊打开机器人主动消息:" + str(event))
32 |
33 |
34 | if __name__ == "__main__":
35 | # 通过预设置的类型,设置需要监听的事件通道
36 | # intents = botpy.Intents.none()
37 | # intents.public_messages=True
38 |
39 | # 通过kwargs,设置需要监听的事件通道
40 | intents = botpy.Intents(public_messages=True)
41 | client = MyClient(intents=intents)
42 | client.run(appid=test_config["appid"], secret=test_config["secret"])
43 |
--------------------------------------------------------------------------------
/examples/demo_group_reply_file.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import asyncio
3 | import os
4 |
5 | import botpy
6 | from botpy import logging
7 | from botpy.ext.cog_yaml import read
8 | from botpy.message import GroupMessage, Message
9 |
10 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
11 |
12 | _log = logging.get_logger()
13 |
14 |
15 | class MyClient(botpy.Client):
16 | async def on_ready(self):
17 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
18 |
19 | async def on_group_at_message_create(self, message: GroupMessage):
20 | file_url = "" # 这里需要填写上传的资源Url
21 | uploadMedia = await message._api.post_group_file(
22 | group_openid=message.group_openid,
23 | file_type=1, # 文件类型要对应上,具体支持的类型见方法说明
24 | url=file_url # 文件Url
25 | )
26 |
27 | # 资源上传后,会得到Media,用于发送消息
28 | await message._api.post_group_message(
29 | group_openid=message.group_openid,
30 | msg_type=7, # 7表示富媒体类型
31 | msg_id=message.id,
32 | media=uploadMedia
33 | )
34 |
35 | if __name__ == "__main__":
36 | # 通过预设置的类型,设置需要监听的事件通道
37 | # intents = botpy.Intents.none()
38 | # intents.public_messages=True
39 |
40 | # 通过kwargs,设置需要监听的事件通道
41 | intents = botpy.Intents(public_messages=True)
42 | client = MyClient(intents=intents)
43 | client.run(appid=test_config["appid"], secret=test_config["secret"])
--------------------------------------------------------------------------------
/examples/demo_group_reply_text.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import asyncio
3 | import os
4 |
5 | import botpy
6 | from botpy import logging
7 | from botpy.ext.cog_yaml import read
8 | from botpy.message import GroupMessage, Message
9 |
10 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
11 |
12 | _log = logging.get_logger()
13 |
14 |
15 | class MyClient(botpy.Client):
16 | async def on_ready(self):
17 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
18 |
19 | async def on_group_at_message_create(self, message: GroupMessage):
20 | messageResult = await message._api.post_group_message(
21 | group_openid=message.group_openid,
22 | msg_type=0,
23 | msg_id=message.id,
24 | content=f"收到了消息:{message.content}")
25 | _log.info(messageResult)
26 |
27 |
28 | if __name__ == "__main__":
29 | # 通过预设置的类型,设置需要监听的事件通道
30 | # intents = botpy.Intents.none()
31 | # intents.public_messages=True
32 |
33 | # 通过kwargs,设置需要监听的事件通道
34 | intents = botpy.Intents(public_messages=True)
35 | client = MyClient(intents=intents)
36 | client.run(appid=test_config["appid"], secret=test_config["secret"])
--------------------------------------------------------------------------------
/examples/demo_guild_member_event.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.user import Member
8 | from botpy.ext.cog_yaml import read
9 |
10 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
11 |
12 | _log = logging.get_logger()
13 |
14 |
15 | class MyClient(botpy.Client):
16 | async def on_ready(self):
17 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
18 |
19 | async def on_guild_member_add(self, member: Member):
20 | _log.info("%s 加入频道" % member.nick)
21 | dms_payload = await self.api.create_dms(member.guild_id, member.user.id)
22 | _log.info("发送私信")
23 | await self.api.post_dms(dms_payload["guild_id"], content="welcome join guild", msg_id=member.event_id)
24 |
25 | async def on_guild_member_update(self, member: Member):
26 | _log.info("%s 更新了资料" % member.nick)
27 |
28 | async def on_guild_member_remove(self, member: Member):
29 | _log.info("%s 退出了频道" % member.nick)
30 |
31 |
32 | if __name__ == "__main__":
33 | # 通过预设置的类型,设置需要监听的事件通道
34 | # intents = botpy.Intents.none()
35 | # intents.public_guild_messages=True
36 |
37 | # 通过kwargs,设置需要监听的事件通道
38 | intents = botpy.Intents(guild_members=True)
39 | client = MyClient(intents=intents)
40 | client.run(appid=test_config["appid"], secret=test_config["secret"])
41 |
--------------------------------------------------------------------------------
/examples/demo_open_forum_event.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import botpy
3 | from botpy import logging
4 | from botpy.forum import OpenThread
5 |
6 | _log = logging.get_logger()
7 |
8 |
9 | class MyClient(botpy.Client):
10 | async def on_ready(self):
11 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
12 |
13 | async def on_open_forum_thread_create(self, open_forum_thread: OpenThread):
14 | _log.info("%s 创建了主题" % open_forum_thread.author_id)
15 |
16 | async def on_open_forum_thread_update(self, open_forum_thread: OpenThread):
17 | _log.info("%s 更新了主题" % open_forum_thread.author_id)
18 |
19 | async def on_open_forum_thread_delete(self, open_forum_thread: OpenThread):
20 | _log.info("%s 删除了主题" % open_forum_thread.author_id)
21 |
22 | async def on_open_forum_post_create(self, open_forum_thread: OpenThread):
23 | _log.info("%s 创建了帖子" % open_forum_thread.author_id)
24 |
25 | async def on_open_forum_post_delete(self, open_forum_thread: OpenThread):
26 | _log.info("%s 删除了帖子" % open_forum_thread.author_id)
27 |
28 | async def on_open_forum_reply_create(self, open_forum_thread: OpenThread):
29 | _log.info("%s 发表了评论" % open_forum_thread.author_id)
30 |
31 | async def on_open_forum_reply_delete(self, open_forum_thread: OpenThread):
32 | _log.info("%s 删除了评论" % open_forum_thread.author_id)
33 |
34 |
35 | if __name__ == "__main__":
36 | # 通过预设置的类型,设置需要监听的事件通道
37 | # intents = botpy.Intents.none()
38 | # intents.public_guild_messages=True
39 |
40 | # 通过kwargs,设置需要监听的事件通道
41 | intents = botpy.Intents(open_forum_event=True)
42 | client = MyClient(intents=intents)
43 | client.run(appid="appid", secret="secret")
44 |
--------------------------------------------------------------------------------
/examples/demo_pins_message.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.message import Message
8 | from botpy.ext.cog_yaml import read
9 |
10 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
11 |
12 | _log = logging.get_logger()
13 |
14 |
15 | class MyClient(botpy.Client):
16 | async def on_ready(self):
17 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
18 |
19 | async def on_at_message_create(self, message: Message):
20 | await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}")
21 | if "/获取精华列表" in message.content:
22 | pins_message = await self.api.get_pins(message.channel_id)
23 | _log.info(pins_message)
24 |
25 | if "/创建精华消息" in message.content:
26 | pins_message = await self.api.put_pin(message.channel_id, message.id)
27 | _log.info(pins_message)
28 |
29 | if "/删除精华消息" in message.content:
30 | result = await self.api.delete_pin(message.channel_id, message.id)
31 | _log.info(result)
32 |
33 |
34 | if __name__ == "__main__":
35 | # 通过预设置的类型,设置需要监听的事件通道
36 | # intents = botpy.Intents.none()
37 | # intents.public_guild_messages=True
38 |
39 | # 通过kwargs,设置需要监听的事件通道
40 | intents = botpy.Intents(public_guild_messages=True)
41 | client = MyClient(intents=intents)
42 | client.run(appid=test_config["appid"], secret=test_config["secret"])
43 |
--------------------------------------------------------------------------------
/examples/demo_recall.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | import botpy
5 | from botpy import logging
6 |
7 | from botpy.message import Message
8 | from botpy.ext.cog_yaml import read
9 |
10 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
11 |
12 | _log = logging.get_logger()
13 |
14 |
15 | class MyClient(botpy.Client):
16 | async def on_ready(self):
17 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
18 |
19 | async def on_at_message_create(self, message: Message):
20 | _message = await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}")
21 | await self.api.recall_message(message.channel_id, _message.get("id"), hidetip=True)
22 |
23 |
24 | if __name__ == "__main__":
25 | # 通过预设置的类型,设置需要监听的事件通道
26 | # intents = botpy.Intents.none()
27 | # intents.public_guild_messages=True
28 |
29 | # 通过kwargs,设置需要监听的事件通道
30 | intents = botpy.Intents(public_guild_messages=True)
31 | client = MyClient(intents=intents)
32 | client.run(appid=test_config["appid"], secret=test_config["secret"])
33 |
--------------------------------------------------------------------------------
/examples/demo_schedule.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import time
4 |
5 | import botpy
6 | from botpy import logging
7 |
8 | from botpy.message import Message
9 | from botpy.ext.cog_yaml import read
10 |
11 | test_config = read(os.path.join(os.path.dirname(__file__), "config.yaml"))
12 |
13 | _log = logging.get_logger()
14 |
15 | CHANNEL_SCHEDULE_ID = "12333" # 修改为自己频道的日程子频道ID
16 |
17 |
18 | class MyClient(botpy.Client):
19 | async def on_ready(self):
20 | _log.info(f"robot 「{self.robot.name}」 on_ready!")
21 |
22 | async def on_at_message_create(self, message: Message):
23 | schedule_id: str = "" # 日程ID,可以填写或者发送/创建日程 命令后获取
24 | _log.info("receive message %s" % message.content)
25 | # 先发送消息告知用户
26 | await message.reply(content=f"机器人{self.robot.name}收到你的@消息了: {message.content}")
27 |
28 | delay = 1000 * 60
29 | start_time = int(round(time.time() * 1000)) + delay
30 | end_time = start_time + delay
31 |
32 | # 判断用户@后输出的指令
33 | if "/创建日程" in message.content:
34 | schedule = await self.api.create_schedule(
35 | CHANNEL_SCHEDULE_ID,
36 | name="test",
37 | start_timestamp=str(start_time),
38 | end_timestamp=str(end_time),
39 | jump_channel_id=CHANNEL_SCHEDULE_ID,
40 | remind_type="0",
41 | )
42 | schedule_id = schedule.id
43 |
44 | elif "/查询日程" in message.content:
45 | schedule = await self.api.get_schedule(CHANNEL_SCHEDULE_ID, schedule_id)
46 | _log.info(schedule)
47 |
48 | elif "/更新日程" in message.content:
49 | await self.api.update_schedule(
50 | CHANNEL_SCHEDULE_ID,
51 | schedule_id,
52 | name="update",
53 | start_timestamp=str(start_time),
54 | end_timestamp=str(end_time),
55 | jump_channel_id=CHANNEL_SCHEDULE_ID,
56 | remind_type="0",
57 | )
58 | elif "/删除日程" in message.content:
59 | await self.api.delete_schedule(CHANNEL_SCHEDULE_ID, schedule_id)
60 |
61 |
62 | if __name__ == "__main__":
63 | # 通过预设置的类型,设置需要监听的事件通道
64 | # intents = botpy.Intents.none()
65 | # intents.public_guild_messages=True
66 |
67 | # 通过kwargs,设置需要监听的事件通道
68 | intents = botpy.Intents(public_guild_messages=True)
69 | client = MyClient(intents=intents)
70 | client.run(appid=test_config["appid"], secret=test_config["secret"])
71 |
--------------------------------------------------------------------------------
/examples/resource/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tencent-connect/botpy/e25f3e84bad7217357d8200a9d12939b58285b84/examples/resource/test.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pre-commit
2 | PyYAML
3 | aiohttp>=3.7.4,<4
4 | APScheduler
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore =
3 | ;W503 line break before binary operator
4 | W503,
5 | ;E203 whitespace before ':'
6 | E203,
7 |
8 | ; exclude file
9 | exclude =
10 | .tox,
11 | .git,
12 | __pycache__,
13 | build,
14 | dist,
15 | *.pyc,
16 | *.egg-info,
17 | .cache,
18 | .eggs
19 |
20 | max-line-length = 120
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 |
4 | from setuptools import setup, find_packages
5 |
6 | setup(
7 | name="qq-botpy",
8 | version=os.getenv("VERSION_NAME"),
9 | author="veehou",
10 | author_email="veehou@tencent.com",
11 | description="qq robot client with python3",
12 | long_description=open("README.rst", encoding="utf-8").read(),
13 | # 项目主页
14 | url="https://github.com/tencent-connect/botpy",
15 | # 你要安装的包,通过 setuptools.find_packages 找到当前目录下有哪些包
16 | packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
17 | # 执照
18 | license="Tencent",
19 | # 安装依赖
20 | install_requires=["aiohttp>=3.7.4,<4", "PyYAML", "APScheduler"],
21 | # 分类
22 | classifiers=[
23 | # 发展时期,常见的如下
24 | # 3 - Alpha
25 | # 4 - Beta
26 | # 5 - Production/Stable
27 | "Development Status :: 4 - Beta",
28 | # 开发的目标用户
29 | "Intended Audience :: Developers",
30 | # 属于什么类型
31 | "Topic :: Software Development",
32 | # 许可证信息
33 | "License :: OSI Approved :: MIT License",
34 | # 目标 Python 版本
35 | "Programming Language :: Python :: 3.7",
36 | ],
37 | )
38 |
--------------------------------------------------------------------------------
/tests/.test(demo).yaml:
--------------------------------------------------------------------------------
1 | # test yaml 用于设置test相关的参数,开源版本需要去掉参数
2 | token:
3 | appid: "123"
4 | token: "xxxx"
5 | secret: "xxx"
6 | test_params:
7 | guild_id: "123"
8 | guild_owner_id: "123"
9 | guild_owner_name: "veehou"
10 | guild_test_member_id: "123"
11 | guild_test_role_id: "123"
12 | channel_id: "123"
13 | channel_name: "channel"
14 | channel_schedule_id: "123"
15 | robot_name: "veehou's robot"
16 | is_sandbox: False
17 | message_id: "123"
18 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 |
5 | from botpy.ext.cog_yaml import read
6 |
7 | # github下修改 .test(demo).yaml 为 .test.yaml
8 | test_config = read(os.path.join(os.path.dirname(__file__), ".test.yaml"))
9 |
--------------------------------------------------------------------------------
/tests/test_api.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | import asyncio
4 | import time
5 | import unittest
6 | from typing import List
7 |
8 | import botpy
9 | from botpy import logging, BotHttp, Permission
10 | from botpy.errors import (
11 | AuthenticationFailedError,
12 | ServerError,
13 | )
14 | from botpy.types import guild, user, channel, message
15 | from botpy.types.announce import AnnouncesType
16 | from botpy.types.channel import ChannelType, ChannelSubType
17 | from tests import test_config
18 |
19 | logger = logging.get_logger()
20 |
21 | token = botpy.Token(test_config["token"]["appid"], test_config["token"]["token"])
22 | test_params_ = test_config["test_params"]
23 | GUILD_ID = test_params_["guild_id"]
24 | GUILD_OWNER_ID = test_params_["guild_owner_id"]
25 | GUILD_OWNER_NAME = test_params_["guild_owner_name"]
26 | GUILD_TEST_MEMBER_ID = test_params_["guild_test_member_id"]
27 | GUILD_TEST_ROLE_ID = test_params_["guild_test_role_id"]
28 | CHANNEL_ID = test_params_["channel_id"]
29 | CHANNEL_NAME = test_params_["channel_name"]
30 | CHANNEL_SCHEDULE_ID = test_params_["channel_schedule_id"]
31 | ROBOT_NAME = test_params_["robot_name"]
32 | IS_SANDBOX = test_params_["is_sandbox"]
33 | MESSAGE_ID = test_params_["message_id"]
34 |
35 |
36 | class APITestCase(unittest.TestCase):
37 | def setUp(self) -> None:
38 | print("setUp")
39 | self.loop = asyncio.get_event_loop()
40 | self.http = BotHttp(timeout=5, app_id=test_config["token"]["appid"], secret=test_config["token"]["secret"])
41 | self.api = botpy.BotAPI(self.http)
42 |
43 | def tearDown(self) -> None:
44 | print("tearDown")
45 | self.loop.run_until_complete(self.http.close())
46 |
47 | def test_guild(self):
48 | result: guild.GuildPayload = self.loop.run_until_complete(self.api.get_guild(GUILD_ID))
49 | self.assertNotEqual("", result["name"])
50 |
51 | def test_guild_roles(self):
52 | result: guild.GuildRoles = self.loop.run_until_complete(self.api.get_guild_roles(GUILD_ID))
53 | self.assertEqual(GUILD_ID, result["guild_id"])
54 |
55 | def test_guild_role_create_update_delete(self):
56 | coroutine = self.api.create_guild_role(GUILD_ID, name="Test Role", color=4278245297)
57 | result: guild.GuildRole = self.loop.run_until_complete(coroutine)
58 | self.assertEqual("Test Role", result["role"]["name"])
59 | id = result["role"]["id"]
60 |
61 | time.sleep(0.5)
62 | coroutine = self.api.update_guild_role(GUILD_ID, role_id=id, name="Test Update Role")
63 | result = self.loop.run_until_complete(coroutine)
64 | self.assertEqual("Test Update Role", result["role"]["name"])
65 |
66 | time.sleep(0.5)
67 | result = self.loop.run_until_complete(self.api.delete_guild_role(GUILD_ID, role_id=id))
68 | self.assertEqual(None, result)
69 |
70 | def test_guild_role_member_add_delete(self):
71 | coroutine = self.api.create_guild_role(GUILD_ID, name="Test Role Member", color=4278245297)
72 | result: guild.GuildRole = self.loop.run_until_complete(coroutine)
73 | self.assertEqual("Test Role Member", result["role"]["name"])
74 | id = result["role"]["id"]
75 |
76 | time.sleep(0.5)
77 | result = self.loop.run_until_complete(
78 | self.api.create_guild_role_member(GUILD_ID, id, GUILD_TEST_MEMBER_ID),
79 | )
80 | self.assertEqual(None, result)
81 |
82 | time.sleep(0.5)
83 | result = self.loop.run_until_complete(
84 | self.api.delete_guild_role_member(GUILD_ID, id, GUILD_TEST_MEMBER_ID),
85 | )
86 | self.assertEqual(None, result)
87 |
88 | time.sleep(0.5)
89 | result = self.loop.run_until_complete(self.api.delete_guild_role(GUILD_ID, role_id=id))
90 | self.assertEqual(None, result)
91 |
92 | def test_guild_member(self):
93 | member: user.Member = self.loop.run_until_complete(self.api.get_guild_member(GUILD_ID, GUILD_OWNER_ID))
94 | self.assertEqual(GUILD_OWNER_NAME, member["user"]["username"])
95 |
96 | def test_guild_members(self):
97 | try:
98 | members = self.loop.run_until_complete(self.api.get_guild_members(GUILD_ID))
99 | print(members)
100 | except AuthenticationFailedError as e:
101 | print(e.args)
102 |
103 | def test_channel(self):
104 | result: channel.ChannelPayload = self.loop.run_until_complete(self.api.get_channel(CHANNEL_ID))
105 | self.assertEqual(CHANNEL_NAME, result["name"])
106 |
107 | def test_channels(self):
108 | result: List[channel.ChannelPayload] = self.loop.run_until_complete(self.api.get_channels(GUILD_ID))
109 | self.assertNotEqual(0, len(result))
110 |
111 | def test_create_update_delete_channel(self):
112 | # create
113 | coro = self.api.create_channel(GUILD_ID, "channel_test", ChannelType.TEXT_CHANNEL, ChannelSubType.TALK)
114 | result: channel.ChannelPayload = self.loop.run_until_complete(coro)
115 | # patch
116 | coro = self.api.update_channel(result["id"], name="update_channel")
117 | result: channel.ChannelPayload = self.loop.run_until_complete(coro)
118 | self.assertEqual("update_channel", result["name"])
119 | # delete
120 | coro = self.api.delete_channel(result["id"])
121 | delete_channel: channel.ChannelPayload = self.loop.run_until_complete(coro)
122 | self.assertTrue(result["name"], delete_channel["name"])
123 |
124 | def test_channel_permissions(self):
125 | coroutine = self.api.get_channel_user_permissions(CHANNEL_ID, GUILD_OWNER_ID)
126 | channel_permissions: channel.ChannelPermissions = self.loop.run_until_complete(coroutine)
127 | # 可查看、可发言、可管理
128 | self.assertEqual("7", channel_permissions["permissions"])
129 |
130 | def test_channel_permissions_update(self):
131 | remove = Permission(manager_permission=True)
132 | coroutine = self.api.update_channel_user_permissions(CHANNEL_ID, GUILD_TEST_MEMBER_ID, remove=remove)
133 | result = self.loop.run_until_complete(coroutine)
134 | self.assertEqual(None, result)
135 |
136 | def test_channel_role_permissions(self):
137 | coroutine = self.api.get_channel_role_permissions(CHANNEL_ID, GUILD_TEST_ROLE_ID)
138 | channel_permissions: channel.ChannelPermissions = self.loop.run_until_complete(coroutine)
139 | self.assertEqual("0", channel_permissions["permissions"])
140 |
141 | def test_channel_role_permissions_update(self):
142 | add = Permission(manager_permission=True)
143 | coroutine = self.api.update_channel_role_permissions(CHANNEL_ID, GUILD_TEST_MEMBER_ID, add=add)
144 | result = self.loop.run_until_complete(coroutine)
145 | self.assertEqual(None, result)
146 |
147 | def test_me(self):
148 | user = self.loop.run_until_complete(self.api.me())
149 | self.assertEqual(ROBOT_NAME, user["username"])
150 |
151 | def test_me_guilds(self):
152 | guilds = self.loop.run_until_complete(self.api.me_guilds())
153 | self.assertGreaterEqual(len(guilds), 1)
154 |
155 | guilds = self.loop.run_until_complete(self.api.me_guilds(GUILD_ID, limit=1, desc=True))
156 | self.assertLessEqual(len(guilds), 1)
157 |
158 | def test_post_audio(self):
159 | payload = {"audio_url": "test", "text": "test", "status": 0}
160 | try:
161 | result = self.loop.run_until_complete(self.api.update_audio(CHANNEL_ID, payload))
162 | print(result)
163 | except (AuthenticationFailedError, ServerError) as e:
164 | print(e)
165 |
166 | def test_create_and_send_dms(self):
167 | payload: message.DmsPayload = self.loop.run_until_complete(self.api.create_dms(GUILD_ID, GUILD_OWNER_ID))
168 | self.assertIsNotNone(payload["guild_id"])
169 | self.loop.run_until_complete(self.api.post_dms(payload["guild_id"], content="test", msg_id=MESSAGE_ID))
170 | # 私信有限制频率
171 | # self.assertTrue("test", _message["content"])
172 |
173 | def test_ws(self):
174 | ws = self.loop.run_until_complete(self.api.get_ws_url())
175 | self.assertEqual(ws["url"], "wss://api.sgroup.qq.com/websocket")
176 |
177 | def test_mute_all(self):
178 | result = self.loop.run_until_complete(self.api.mute_all(GUILD_ID, mute_seconds="20"))
179 | self.assertEqual(None, result)
180 |
181 | def test_mute_member(self):
182 | result = self.loop.run_until_complete(self.api.mute_member(GUILD_ID, GUILD_TEST_MEMBER_ID, mute_seconds="20"))
183 | self.assertEqual(None, result)
184 |
185 | def test_mute_multi_member(self):
186 | result: List[str] = self.loop.run_until_complete(
187 | self.api.mute_multi_member(GUILD_ID, mute_seconds="120", user_ids=[GUILD_TEST_MEMBER_ID])
188 | )
189 | self.assertEqual(1, len(result))
190 |
191 | def test_post_recommend_channel(self):
192 | channel_list = [{"channel_id": CHANNEL_ID, "introduce": "introduce"}]
193 | result = self.loop.run_until_complete(
194 | self.api.create_recommend_announce(GUILD_ID, AnnouncesType.MEMBER, channel_list)
195 | )
196 | self.assertEqual(len(channel_list), len(result["recommend_channels"]))
197 |
198 | def test_get_permissions(self):
199 | result = self.loop.run_until_complete(self.api.get_permissions(GUILD_ID))
200 | self.assertNotEqual(0, len(result))
201 |
202 | def test_post_permissions_demand(self):
203 | demand_identity = {"path": "/guilds/{guild_id}/members/{user_id}", "method": "GET"}
204 | result = self.loop.run_until_complete(
205 | self.api.post_permission_demand(GUILD_ID, CHANNEL_ID, api_identify=demand_identity, desc="test")
206 | )
207 | print(result["title"])
208 |
209 | def test_get_schedules(self):
210 | schedules = self.loop.run_until_complete(self.api.get_schedules(CHANNEL_SCHEDULE_ID))
211 | self.assertEqual(None, schedules)
212 |
213 | def test_put_and_delete_reaction(self):
214 | result = self.loop.run_until_complete(self.api.put_reaction(CHANNEL_ID, MESSAGE_ID, 1, "4"))
215 | self.assertEqual(None, result)
216 |
217 | time.sleep(1) # 表情表态操作有频率限制,中间隔一秒
218 |
219 | result = self.loop.run_until_complete(self.api.delete_reaction(CHANNEL_ID, MESSAGE_ID, 1, "4"))
220 | self.assertEqual(None, result)
221 |
222 | def test_get_reaction_users(self):
223 | result = self.loop.run_until_complete(self.api.get_reaction_users(CHANNEL_ID, MESSAGE_ID, 1, "4"))
224 | self.assertEqual(result["is_end"], True)
225 |
226 | def test_put_and_delete_pin(self):
227 | result = self.loop.run_until_complete(self.api.put_pin(CHANNEL_ID, MESSAGE_ID))
228 | self.assertIsNotNone(result)
229 |
230 | result = self.loop.run_until_complete(self.api.delete_pin(CHANNEL_ID, MESSAGE_ID))
231 | self.assertEqual(None, result)
232 |
233 | def test_get_pins(self):
234 | result = self.loop.run_until_complete(self.api.get_pins(CHANNEL_ID))
235 | self.assertTrue(len(result["message_ids"]) >= 0)
236 |
237 |
238 | if __name__ == "__main__":
239 | unittest.main()
240 |
--------------------------------------------------------------------------------
/tests/test_flags.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | import botpy
4 |
5 |
6 | class IntentsTestCase(unittest.TestCase):
7 | def test_none(self):
8 | intents = botpy.Intents.none()
9 | self.assertEqual(intents.value, 0) # add assertion here
10 |
11 | def test_multi_intents(self):
12 | intents = botpy.Intents(guilds=True, guild_messages=True)
13 | self.assertEqual(513, intents.value)
14 |
15 | def test_default(self):
16 | intents = botpy.Intents.default()
17 | self.assertEqual(1846285315, intents.value)
18 |
19 |
20 | if __name__ == "__main__":
21 | unittest.main()
22 |
--------------------------------------------------------------------------------
/tests/test_token.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import unittest
5 |
6 | from botpy.robot import Token
7 |
8 |
9 | class MyTestCase(unittest.TestCase):
10 | def test_something(self):
11 | token = Token("123", "123")
12 | self.assertEqual(token.app_id, "123")
13 | self.assertEqual(token.secret, "123")
14 |
15 |
16 | if __name__ == "__main__":
17 | unittest.main()
18 |
--------------------------------------------------------------------------------