├── .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 | ![botpy](https://socialify.git.ci/tencent-connect/botpy/image?description=1&font=Source%20Code%20Pro&forks=1&issues=1&language=1&logo=https%3A%2F%2Fgithub.com%2Ftencent-connect%2Fbot-docs%2Fblob%2Fmain%2Fdocs%2F.vuepress%2Fpublic%2Ffavicon-64px.png%3Fraw%3Dtrue&owner=1&pattern=Circuit%20Board&pulls=1&stargazers=1&theme=Light) 4 | 5 | [![Language](https://img.shields.io/badge/language-python-green.svg?style=plastic)](https://www.python.org/) 6 | [![License](https://img.shields.io/badge/license-MIT-orange.svg?style=plastic)](https://github.com/tencent-connect/botpy/blob/master/LICENSE) 7 | ![Python](https://img.shields.io/badge/python-3.8+-blue) 8 | ![PyPI](https://img.shields.io/pypi/v/qq-botpy) 9 | [![BK Pipelines Status](https://api.bkdevops.qq.com/process/api/external/pipelines/projects/qq-guild-open/p-713959939bdc4adca0eea2d4420eef4b/badge?X-DEVOPS-PROJECT-ID=qq-guild-open)](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 | ![开发者社区](https://guild-1251316161.cos.ap-guangzhou.myqcloud.com/miniapp/icons/qq_guild_developer_doc.png) 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 | --------------------------------------------------------------------------------