mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 19:26:44 +00:00
Compare commits
448 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
890b2ee22f | ||
|
408292d679 | ||
|
ec4761c3a9 | ||
|
0091a03653 | ||
|
e1a63f980f | ||
|
d982e14793 | ||
|
43933920ed | ||
|
fc03c58c70 | ||
|
283560daa7 | ||
|
efc4f5a0d5 | ||
|
9f707469da | ||
|
6396a7558a | ||
|
a8a76393a5 | ||
|
0d0bc656c8 | ||
|
2a2f7b6dce | ||
|
17c86f7da2 | ||
|
1213e89bf5 | ||
|
ae08568daf | ||
|
8fbc85cf50 | ||
|
315dcb329e | ||
|
438e4f57e3 | ||
|
a346efd684 | ||
|
e3151c5f5e | ||
|
47536e6554 | ||
|
79ac7f024f | ||
|
3709e0ba4f | ||
|
c8ffafc1e8 | ||
|
fedb67d4ae | ||
|
076611166a | ||
|
d5234e44f5 | ||
|
64f78c279a | ||
|
744443ab18 | ||
|
9e5cde490e | ||
|
080c0db64b | ||
|
ec41b5f57f | ||
|
c441ec7080 | ||
|
8bb92309d5 | ||
|
9ab7666b6d | ||
|
add1f1473d | ||
|
cba38c399b | ||
|
18beb63d55 | ||
|
8977be2985 | ||
|
00686380b8 | ||
|
d4da953ad8 | ||
|
e5de8c8053 | ||
|
c2a2f8d420 | ||
|
af2ea7b83a | ||
|
909a335106 | ||
|
3e92fccd4e | ||
|
9afaf3d516 | ||
|
8ca56f24e3 | ||
|
8d09dd97f5 | ||
|
78bbf9e623 | ||
|
05a6af46b9 | ||
|
243ad3f896 | ||
|
709c36bf5f | ||
|
ba808c85d5 | ||
|
c5444799f5 | ||
|
36e99bc3ea | ||
|
f65127e655 | ||
|
600c4f3268 | ||
|
53898dfb51 | ||
|
95e3650c51 | ||
|
9f1b9ce2f3 | ||
|
551963c6c3 | ||
|
d59c999554 | ||
|
8f44df371a | ||
|
7822cabe32 | ||
|
ca0b17b46a | ||
|
d1404f6004 | ||
|
a294f0fbe0 | ||
|
3cd0066715 | ||
|
faaef1a387 | ||
|
ad4b244701 | ||
|
51e7bae8f2 | ||
|
f18b6f609e | ||
|
dfbb32937e | ||
|
cf9788ec99 | ||
|
6b83d03094 | ||
|
5508c1a4ee | ||
|
3462295562 | ||
|
fee16082e0 | ||
|
926b257065 | ||
|
fca2d074e0 | ||
|
1a473f171c | ||
|
97eee5a2f7 | ||
|
8f1fbd9b36 | ||
|
856f0b981f | ||
|
f629fc9309 | ||
|
e617bf2762 | ||
|
072c2a2a41 | ||
|
3a142033a1 | ||
|
2832514f49 | ||
|
e7887056b9 | ||
|
500b59905d | ||
|
4d4074ca24 | ||
|
8391de52d9 | ||
|
dd5f3bdea1 | ||
|
c4bfe3a823 | ||
|
3ec76454e3 | ||
|
0e481d96a6 | ||
|
c2b8bbee5f | ||
|
61ad0733de | ||
|
06fa0fb860 | ||
|
e9b1692124 | ||
|
653902c6a2 | ||
|
db0fcc0ceb | ||
|
f0021af6d4 | ||
|
7e614bb2b7 | ||
|
adba6c1890 | ||
|
6116d394e5 | ||
|
31ea5fa306 | ||
|
b209b77235 | ||
|
81870e0d64 | ||
|
40bccbc585 | ||
|
46c2817bba | ||
|
a5302a1872 | ||
|
6642500c1c | ||
|
f9464171fd | ||
|
34d307b881 | ||
|
8dc36aa630 | ||
|
f324b62eb2 | ||
|
a21b511568 | ||
|
df1c13accd | ||
|
c141b3eae7 | ||
|
a2a5af9b5e | ||
|
86a4f4043e | ||
|
f3aa8c6aa5 | ||
|
be81d094b4 | ||
|
d0f832c4cd | ||
|
317a2b8c9b | ||
|
433c672130 | ||
|
f8c67ebdf6 | ||
|
2a95588421 | ||
|
a5fc40f2dc | ||
|
e4ccb683cc | ||
|
34223d6b37 | ||
|
04a7c3bc13 | ||
|
dd04190ca2 | ||
|
5fd5b2f5b3 | ||
|
e8ad79aaf3 | ||
|
ec8bc0424e | ||
|
6063714093 | ||
|
1a0976e834 | ||
|
74743e6176 | ||
|
1befd9ffc6 | ||
|
c688450690 | ||
|
d9e7986f5c | ||
|
d9bdf38a4e | ||
|
1bd1f15c49 | ||
|
b4083ff9f9 | ||
|
2e766a86c2 | ||
|
93976a4162 | ||
|
4dc92ffc0b | ||
|
5747397790 | ||
|
17fb3f92eb | ||
|
8f79ba1ccd | ||
|
e11298d15b | ||
|
728902bfcf | ||
|
97723a0838 | ||
|
e42111f31f | ||
|
8d4eb7faf8 | ||
|
9b19bf63b2 | ||
|
6685d88b44 | ||
|
694db6c278 | ||
|
1dc02bfe8e | ||
|
045ab60699 | ||
|
dc5ebd7a90 | ||
|
676e729df8 | ||
|
dda6b97ef8 | ||
|
b3d22ea8c4 | ||
|
eac72d2d48 | ||
|
955ada47ed | ||
|
4c31683231 | ||
|
a8f3d83947 | ||
|
87e1866cf4 | ||
|
0d39b788bb | ||
|
635668e6d4 | ||
|
6358d07fbd | ||
|
c03e4161c7 | ||
|
4c0d4065c5 | ||
|
720c736343 | ||
|
6d7435bf36 | ||
|
423e055ecd | ||
|
b952f325d2 | ||
|
3c3331a1ef | ||
|
4679e7d9cb | ||
|
cbcd3987d2 | ||
|
d8cc1bd644 | ||
|
09b0b2084d | ||
|
6bc2870ea5 | ||
|
f14580e688 | ||
|
5ae24313f7 | ||
|
14088c6c51 | ||
|
73126535ef | ||
|
22e27cce5f | ||
|
8abab79cfc | ||
|
6b419dc929 | ||
|
8bf8b4760b | ||
|
678c1e1532 | ||
|
325d28fbd4 | ||
|
6bff6e9ad3 | ||
|
d58c1407b5 | ||
|
e8760b6e4a | ||
|
4c92890265 | ||
|
d2f000bb16 | ||
|
b534b3b03f | ||
|
485d6e94b4 | ||
|
2f5ba409ec | ||
|
e2e9bcc260 | ||
|
6e214efde8 | ||
|
da72d20d0e | ||
|
646ab1002f | ||
|
1b4e5d05ab | ||
|
f323feac1b | ||
|
cb715c132f | ||
|
9e1b439128 | ||
|
f07e9bf699 | ||
|
92e410ce0a | ||
|
e14709c7f0 | ||
|
0533058dc2 | ||
|
896e988f0e | ||
|
b39fe9f1a9 | ||
|
02301f6098 | ||
|
c5a0f0bd6d | ||
|
f59d5f0826 | ||
|
c369475603 | ||
|
18e698f2a0 | ||
|
33bab04a5a | ||
|
3cf31998b9 | ||
|
c6d85ac9b0 | ||
|
27286fdc57 | ||
|
735cf9db6b | ||
|
d457bece3d | ||
|
37b2a100d0 | ||
|
cd4037c4fe | ||
|
13a490de0d | ||
|
cec09397cc | ||
|
a11ac82a91 | ||
|
6410af19ba | ||
|
8af08c6417 | ||
|
801d29f66c | ||
|
c518f768d1 | ||
|
27149108d9 | ||
|
5d1582566a | ||
|
eceef1ebec | ||
|
50aa8c53e0 | ||
|
558f740c13 | ||
|
8bffba7efd | ||
|
ce93ea13e7 | ||
|
174182d62a | ||
|
36f047be7f | ||
|
9d73af0513 | ||
|
ecb0d78011 | ||
|
5920efb6c5 | ||
|
5893fbe57d | ||
|
27557af636 | ||
|
b37c7995cb | ||
|
f46addbb85 | ||
|
6f57a290d7 | ||
|
ae66e45287 | ||
|
03cf7f290a | ||
|
f203aaf4ca | ||
|
9a2edbbeb1 | ||
|
bd9ca99f63 | ||
|
8be262d305 | ||
|
b92d47b362 | ||
|
bdf8cb0d57 | ||
|
0cb65214c6 | ||
|
ccc2c5676a | ||
|
6daec67ebd | ||
|
051851faed | ||
|
8d2fca3e12 | ||
|
76f37c485c | ||
|
0c7af0873f | ||
|
31fa4ec5f4 | ||
|
fda490d252 | ||
|
40e443fd1a | ||
|
4a17e581d2 | ||
|
081d212487 | ||
|
3d6774136f | ||
|
fa934a156a | ||
|
bac5356a90 | ||
|
b289065f71 | ||
|
09cf0f29ba | ||
|
244837dd7c | ||
|
a0bc113912 | ||
|
6f6a296105 | ||
|
a0d316127f | ||
|
f0c0d7788f | ||
|
3f7e2604f1 | ||
|
f43c0087f7 | ||
|
e71d841045 | ||
|
a3af8da331 | ||
|
8bdfdaef91 | ||
|
afd13ed65d | ||
|
d83751d0ca | ||
|
63dd3b8fa7 | ||
|
ae689605a5 | ||
|
bbaba1c955 | ||
|
f1ee54e5c9 | ||
|
6f8e532afe | ||
|
6f68ff61e5 | ||
|
a930fc0997 | ||
|
65da0947fe | ||
|
1b64a54421 | ||
|
d4e1bb7bf3 | ||
|
d737679ccd | ||
|
4cef5512ee | ||
|
1d5d1602f0 | ||
|
87e767fa25 | ||
|
c38437a22f | ||
|
cafb7bedb4 | ||
|
ace053f387 | ||
|
d6e176d03b | ||
|
2fca5b9664 | ||
|
cd93ace0dd | ||
|
b118cb6f22 | ||
|
a69ccb4e6c | ||
|
d5ec31d0a0 | ||
|
62560635b2 | ||
|
c00430c53f | ||
|
1dcda4bd77 | ||
|
b60035f0e6 | ||
|
8551b13eab | ||
|
b448a6e083 | ||
|
956b202087 | ||
|
b95d49cd9c | ||
|
006b9dd816 | ||
|
a9125cd696 | ||
|
ee5dcf0d42 | ||
|
f13c1cc980 | ||
|
16c0a87929 | ||
|
39d1554905 | ||
|
37067229b0 | ||
|
5ca708d3f4 | ||
|
53dded52a7 | ||
|
f8cee790e7 | ||
|
10447ff3c4 | ||
|
f08aec7894 | ||
|
69edb98835 | ||
|
c73ca2b43f | ||
|
848c6c5061 | ||
|
58f82bf881 | ||
|
9b3e670cee | ||
|
f0bebb65b4 | ||
|
bc845c94e2 | ||
|
f4668bf0bc | ||
|
3493d69fcd | ||
|
125bcb943f | ||
|
e65eb3fb18 | ||
|
8045420f97 | ||
|
03a90006e5 | ||
|
adbe341076 | ||
|
2a623b1c81 | ||
|
c91926aea6 | ||
|
63329257de | ||
|
c9dc6e648e | ||
|
78a818547e | ||
|
41eed9d0e9 | ||
|
e32019f15d | ||
|
59c033e2dd | ||
|
49cf1ec5d3 | ||
|
e5ad15d6d6 | ||
|
4cf9790a95 | ||
|
7467e66dab | ||
|
516c1c220c | ||
|
136778ae5b | ||
|
17538f7a66 | ||
|
d1da0be0da | ||
|
e92bc24631 | ||
|
73d1e5dd88 | ||
|
ea83ba78ec | ||
|
712f80a307 | ||
|
5c4ef8fc00 | ||
|
2b0973c9f5 | ||
|
767a8ecb08 | ||
|
b342940b69 | ||
|
7880f07db4 | ||
|
ac702d7eb7 | ||
|
9f3b3b2c32 | ||
|
2d08465426 | ||
|
827d8fbc0e | ||
|
0320be1947 | ||
|
aca65954bd | ||
|
1da9376fc8 | ||
|
909b811f68 | ||
|
ceecf9c692 | ||
|
3de2922773 | ||
|
a5d26b7747 | ||
|
932f50d1fb | ||
|
c69619f142 | ||
|
5a49d1e0e2 | ||
|
4f9f3f449c | ||
|
60733c97be | ||
|
7a1aa0c204 | ||
|
6d1383a10c | ||
|
86006fafdc | ||
|
44ca4f729a | ||
|
cf29209a55 | ||
|
5e78e2bb5d | ||
|
440e15e204 | ||
|
6711a84cab | ||
|
5c2d2141e3 | ||
|
8ec1552fd6 | ||
|
c1dca723ae | ||
|
b6cd0424fa | ||
|
1beee94c1d | ||
|
f2a618f663 | ||
|
c4286f1f39 | ||
|
cd9e30bd52 | ||
|
24ae0dfa15 | ||
|
19a20a3407 | ||
|
36d7b44741 | ||
|
8176cd189c | ||
|
b25de93eb1 | ||
|
79e636ad89 | ||
|
9c9973d8d8 | ||
|
04d4954d50 | ||
|
2c81dc1975 | ||
|
850096ceaa | ||
|
6bb15f6533 | ||
|
92c6a8dd6e | ||
|
723eef10bb | ||
|
06c33ad6ea | ||
|
9826bc29ca | ||
|
f35b5b57b7 | ||
|
433b50c79c | ||
|
89f3496a2a | ||
|
df7e8f6d83 | ||
|
b57f17d447 | ||
|
665de1da3e | ||
|
091232e996 | ||
|
9ee2d94f3c | ||
|
78bdfe65ba | ||
|
5006cf7be6 | ||
|
a818e0056e | ||
|
3efae8bfbc | ||
|
024d97b997 | ||
|
c90ab949d2 | ||
|
e8ffa63b78 | ||
|
6c27ec7357 | ||
|
9bf08593d7 | ||
|
b016a59a38 | ||
|
794395e737 | ||
|
a758e6f06e | ||
|
11feb2c0d0 | ||
|
59a2ed7c2e |
@@ -2,7 +2,7 @@
|
||||
"name": "Default Linux Universal",
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2-linux",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:1": {}
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
|
||||
},
|
||||
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && yarn install",
|
||||
"customizations": {
|
||||
|
5
.github/actions/build-api-doc/action.yml
vendored
5
.github/actions/build-api-doc/action.yml
vendored
@@ -5,7 +5,10 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- run: |
|
||||
poetry run nb-autodoc nonebot
|
||||
poetry run nb-autodoc nonebot \
|
||||
-s nonebot.plugins \
|
||||
-u nonebot.internal \
|
||||
-u nonebot.internal.*
|
||||
cp -r ./build/nonebot/* ./website/docs/api/
|
||||
yarn prettier
|
||||
shell: bash
|
||||
|
2
.github/actions/setup-python/action.yml
vendored
2
.github/actions/setup-python/action.yml
vendored
@@ -11,7 +11,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install poetry
|
||||
run: pipx install poetry
|
||||
run: pipx install poetry==1.3.2
|
||||
shell: bash
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
|
4
.github/workflows/codecov.yml
vendored
4
.github/workflows/codecov.yml
vendored
@@ -5,6 +5,10 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
paths:
|
||||
- "nonebot/**"
|
||||
- "packages/**"
|
||||
- "tests/**"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
37
.github/workflows/publish-bot.yml
vendored
37
.github/workflows/publish-bot.yml
vendored
@@ -5,19 +5,47 @@ on:
|
||||
types: [opened, reopened, edited]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
plugin_test:
|
||||
runs-on: ubuntu-latest
|
||||
name: nonebot2 plugin test
|
||||
if: github.event_name != 'issue_comment' || !github.event.issue.pull_request
|
||||
permissions:
|
||||
issues: read
|
||||
outputs:
|
||||
result: ${{ steps.plugin-test.outputs.RESULT }}
|
||||
output: ${{ steps.plugin-test.outputs.OUTPUT }}
|
||||
steps:
|
||||
- name: Install Poetry
|
||||
if: ${{ !startsWith(github.event_name, 'pull_request') }}
|
||||
run: pipx install poetry
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Test Plugin
|
||||
id: plugin-test
|
||||
run: |
|
||||
curl -sSL https://github.com/nonebot/nonebot2-publish-bot/releases/latest/download/plugin_test.py -o plugin_test.py
|
||||
python plugin_test.py
|
||||
publish_bot:
|
||||
runs-on: ubuntu-latest
|
||||
name: nonebot2 publish bot
|
||||
needs: plugin_test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: NoneBot2 Publish Bot
|
||||
uses: docker://ghcr.io/nonebot/nonebot2-publish-bot:main
|
||||
uses: docker://ghcr.io/nonebot/nonebot2-publish-bot:latest
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
config: >
|
||||
@@ -27,3 +55,6 @@ jobs:
|
||||
"bot_path": "website/static/bots.json",
|
||||
"adapter_path": "website/static/adapters.json"
|
||||
}
|
||||
env:
|
||||
PLUGIN_TEST_RESULT: ${{ needs.plugin_test.outputs.result }}
|
||||
PLUGIN_TEST_OUTPUT: ${{ needs.plugin_test.outputs.output }}
|
||||
|
2
.github/workflows/website-deploy.yml
vendored
2
.github/workflows/website-deploy.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v1
|
||||
uses: nwtgck/actions-netlify@v2
|
||||
with:
|
||||
publish-dir: "./website/build"
|
||||
production-deploy: true
|
||||
|
2
.github/workflows/website-preview.yml
vendored
2
.github/workflows/website-preview.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v1
|
||||
uses: nwtgck/actions-netlify@v2
|
||||
with:
|
||||
publish-dir: "./website/build"
|
||||
production-deploy: false
|
||||
|
@@ -6,20 +6,26 @@ ci:
|
||||
autoupdate_schedule: monthly
|
||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/hadialqattan/pycln
|
||||
rev: v2.1.3
|
||||
hooks:
|
||||
- id: pycln
|
||||
args: [--config, pyproject.toml]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.0.0-alpha.4
|
||||
rev: v3.0.0-alpha.6
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
|
||||
|
@@ -66,16 +66,17 @@ yarn start
|
||||
|
||||
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
|
||||
|
||||
以下是比较重要的排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
|
||||
以下是比较重要的编写与排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
|
||||
|
||||
1. 中文与英文、数字、半角符号之间需要有空格。例:`NoneBot2 是一个可扩展的 Python 异步机器人框架。`
|
||||
2. 若非英文整句,使用全角标点符号。例:`现在你可以看到机器人回复你:“Hello, World !”。`
|
||||
3. 直引号`「」`和弯引号`“”`都可接受,但同一份文件里应使用同种引号。
|
||||
4. **不要使用斜体**,你不需要一种与粗体不同的强调。除此之外,你也可以考虑使用 docusaurus 提供的[告示](https://docusaurus.io/zh-CN/docs/markdown-features/admonitions)功能。
|
||||
5. 文档中应以“我们”指代机器人开发者,以“机器人用户”指代机器人的使用者。
|
||||
|
||||
这是社区创始人 richardchien 的中文排版规范,可供参考:<https://stdrc.cc/style-guides/chinese>。
|
||||
以上由[社区创始人 richardchien 的中文排版规范](https://stdrc.cc/style-guides/chinese)补充修改得到。
|
||||
|
||||
如果你需要编辑器提示 Markdown 规范,可以安装 VSCode 上的 markdownlint 插件。
|
||||
如果你需要编辑器检查 Markdown 规范,可以在 VSCode 中安装 markdownlint 扩展。
|
||||
|
||||
### 参与开发
|
||||
|
||||
@@ -83,7 +84,7 @@ NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/
|
||||
|
||||
## 为社区做贡献
|
||||
|
||||
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://v2.nonebot.dev/docs/advanced/publish-plugin) 一节。
|
||||
你可以在 NoneBot 商店上架自己的适配器、插件、机器人,具体步骤可参考 [发布插件](https://v2.nonebot.dev/docs/developer/plugin-publishing) 一节。
|
||||
|
||||
我们仅对插件的兼容性进行简单测试,并会在下一个版本发布前对与该版本不兼容的插件作出处理。
|
||||
|
||||
|
53
README.md
53
README.md
@@ -71,15 +71,13 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<p align="center">
|
||||
<a href="https://v2.nonebot.dev/">文档</a>
|
||||
·
|
||||
<a href="https://v2.nonebot.dev/docs/start/installation">安装</a>
|
||||
·
|
||||
<a href="https://v2.nonebot.dev/docs/tutorial/create-project">开始使用</a>
|
||||
<a href="https://v2.nonebot.dev/docs/quick-start">快速上手</a>
|
||||
·
|
||||
<a href="#插件">文档打不开?</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://asciinema.org/a/464654">
|
||||
<a href="https://asciinema.org/a/569440">
|
||||
<img src="https://v2.nonebot.dev/img/setup.svg">
|
||||
</a>
|
||||
</p>
|
||||
@@ -92,22 +90,24 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
||||
|
||||
- 异步优先:基于 Python 的异步特性,即使是~~非常~~大量的消息,也能吞吐自如
|
||||
- 易于开发:配合 NB-CLI 脚手架,代码编写上手简单,没有过多的冗余代码,可以让开发者专注于业务逻辑
|
||||
- 生而可靠:100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://v2.nonebot.dev/docs/start/editor-support))
|
||||
- 生而可靠:100% 类型注解覆盖,配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中 ([编辑器支持](https://v2.nonebot.dev/docs/editor-support))
|
||||
- 社区丰富:社区用户众多,直接和间接用户超过十万人,每天都有大量的活跃用户 ([社区资源](#社区资源))
|
||||
- 海纳百川:一个框架,支持多个聊天软件平台,可自定义通信协议
|
||||
|
||||
| 协议名称 | 状态 | 注释 |
|
||||
| :--------------------------------------------------------: | :--: | :----------------------------------------------------------------: |
|
||||
| [OneBot 协议](https://onebot.dev/) | ✅ | 支持 QQ、TG、微信公众号等[平台](https://onebot.dev/ecosystem.html) |
|
||||
| [Telegram](https://core.telegram.org/bots/api) | ✅ | |
|
||||
| [飞书](https://open.feishu.cn/document/home/index) | ✅ | |
|
||||
| [GitHub](https://docs.github.com/en/developers/apps) | ✅ | GitHub APP & OAuth APP |
|
||||
| [QQ 频道](https://bot.q.qq.com/wiki/) | ✅ | 官方接口调整较多 |
|
||||
| [钉钉](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer |
|
||||
| Console | ✅ | 控制台交互 |
|
||||
| [开黑啦](https://developer.kookapp.cn/) | ↗️ | 由社区贡献 |
|
||||
| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/) | ↗️ | 由社区贡献 |
|
||||
| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat) | ↗️ | 由社区贡献 |
|
||||
| 协议名称 | 状态 | 注释 |
|
||||
| :-----------------------------------------------------------------------: | :--: | :----------------------------------------------------------------: |
|
||||
| [OneBot 协议](https://onebot.dev/) | ✅ | 支持 QQ、TG、微信公众号等[平台](https://onebot.dev/ecosystem.html) |
|
||||
| [Telegram](https://core.telegram.org/bots/api) | ✅ | |
|
||||
| [飞书](https://open.feishu.cn/document/home/index) | ✅ | |
|
||||
| [GitHub](https://docs.github.com/en/developers/apps) | ✅ | GitHub APP & OAuth APP |
|
||||
| [QQ 频道](https://bot.q.qq.com/wiki/) | ✅ | 官方接口调整较多 |
|
||||
| [钉钉](https://open.dingtalk.com/document/) | 🤗 | 寻找 Maintainer |
|
||||
| Console | ✅ | 控制台交互 |
|
||||
| [开黑啦](https://developer.kookapp.cn/) | ↗️ | 由社区贡献 |
|
||||
| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/) | ↗️ | 由社区贡献 |
|
||||
| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat) | ↗️ | 由社区贡献 |
|
||||
| [MineCraft (Spigot)](https://github.com/17TheWord/nonebot-adapter-spigot) | ↗️ | 由社区贡献 |
|
||||
| [BiliBili Live](https://github.com/wwweww/adapter-bilibili) | ↗️ | 由社区贡献 |
|
||||
|
||||
- 坚实后盾:支持多种 web 框架,可自定义替换、组合
|
||||
|
||||
@@ -135,12 +135,17 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
||||
|
||||
懒得看文档?下面是快速安装指南:
|
||||
|
||||
1. (**强烈建议**)使用你喜欢的 Python 环境管理工具创建新的虚拟环境。
|
||||
|
||||
2. 使用 `pip` (或其他) 安装 NoneBot 脚手架。
|
||||
1. 安装 [pipx](https://pypa.github.io/pipx/)
|
||||
|
||||
```bash
|
||||
pip install nb-cli
|
||||
python -m pip install --user pipx
|
||||
python -m pipx ensurepath
|
||||
```
|
||||
|
||||
2. 安装脚手架
|
||||
|
||||
```bash
|
||||
pipx install nb-cli
|
||||
```
|
||||
|
||||
3. 使用脚手架创建项目
|
||||
@@ -149,6 +154,12 @@ NoneBot2 不是 NoneBot1 的替代品。事实上,它们都在被积极的维
|
||||
nb create
|
||||
```
|
||||
|
||||
4. 运行项目
|
||||
|
||||
```bash
|
||||
nb run
|
||||
```
|
||||
|
||||
## 社区资源
|
||||
|
||||
### 常见问题
|
||||
|
@@ -38,17 +38,16 @@ FrontMatter:
|
||||
"""
|
||||
|
||||
import os
|
||||
import importlib
|
||||
from importlib.metadata import version
|
||||
from typing import Any, Dict, Type, Optional
|
||||
from typing import Any, Dict, Type, Union, TypeVar, Optional, overload
|
||||
|
||||
import loguru
|
||||
from pydantic.env_settings import DotenvType
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.utils import escape_tag
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.log import logger as logger
|
||||
from nonebot.adapters import Bot, Adapter
|
||||
from nonebot.utils import escape_tag, resolve_dot_notation
|
||||
from nonebot.drivers import Driver, ReverseDriver, combine_driver
|
||||
|
||||
try:
|
||||
@@ -56,6 +55,8 @@ try:
|
||||
except Exception: # pragma: no cover
|
||||
__version__ = None
|
||||
|
||||
A = TypeVar("A", bound=Adapter)
|
||||
|
||||
_driver: Optional[Driver] = None
|
||||
|
||||
|
||||
@@ -80,6 +81,56 @@ def get_driver() -> Driver:
|
||||
return _driver
|
||||
|
||||
|
||||
@overload
|
||||
def get_adapter(name: str) -> Adapter:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_adapter(name: Type[A]) -> A:
|
||||
...
|
||||
|
||||
|
||||
def get_adapter(name: Union[str, Type[Adapter]]) -> Adapter:
|
||||
"""获取已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||
|
||||
返回:
|
||||
指定名称或类型的 {ref}`nonebot.adapters.Adapter` 对象
|
||||
|
||||
异常:
|
||||
ValueError: 指定的 {ref}`nonebot.adapters.Adapter` 未注册
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
from nonebot.adapters.console import Adapter
|
||||
adapter = nonebot.get_adapter(Adapter)
|
||||
```
|
||||
"""
|
||||
adapters = get_adapters()
|
||||
target = name if isinstance(name, str) else name.get_name()
|
||||
if target not in adapters:
|
||||
raise ValueError(f"Adapter {target} not registered.")
|
||||
return adapters[target]
|
||||
|
||||
|
||||
def get_adapters() -> Dict[str, Adapter]:
|
||||
"""获取所有已注册的 {ref}`nonebot.adapters.Adapter` 实例。
|
||||
|
||||
返回:
|
||||
所有 {ref}`nonebot.adapters.Adapter` 实例字典
|
||||
|
||||
异常:
|
||||
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
|
||||
|
||||
用法:
|
||||
```python
|
||||
adapters = nonebot.get_adapters()
|
||||
```
|
||||
"""
|
||||
return get_driver()._adapters.copy()
|
||||
|
||||
|
||||
def get_app() -> Any:
|
||||
"""获取全局 {ref}`nonebot.drivers.ReverseDriver` 对应的 Server App 对象。
|
||||
|
||||
@@ -175,31 +226,16 @@ def get_bots() -> Dict[str, Bot]:
|
||||
return get_driver().bots
|
||||
|
||||
|
||||
def _resolve_dot_notation(
|
||||
obj_str: str, default_attr: str, default_prefix: Optional[str] = None
|
||||
) -> Any:
|
||||
modulename, _, cls = obj_str.partition(":")
|
||||
if default_prefix is not None and modulename.startswith("~"):
|
||||
modulename = default_prefix + modulename[1:]
|
||||
module = importlib.import_module(modulename)
|
||||
if not cls:
|
||||
return getattr(module, default_attr)
|
||||
instance = module
|
||||
for attr_str in cls.split("."):
|
||||
instance = getattr(instance, attr_str)
|
||||
return instance
|
||||
|
||||
|
||||
def _resolve_combine_expr(obj_str: str) -> Type[Driver]:
|
||||
drivers = obj_str.split("+")
|
||||
DriverClass = _resolve_dot_notation(
|
||||
DriverClass = resolve_dot_notation(
|
||||
drivers[0], "Driver", default_prefix="nonebot.drivers."
|
||||
)
|
||||
if len(drivers) == 1:
|
||||
logger.trace(f"Detected driver {DriverClass} with no mixins.")
|
||||
return DriverClass
|
||||
mixins = [
|
||||
_resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
|
||||
resolve_dot_notation(mixin, "Mixin", default_prefix="nonebot.drivers.")
|
||||
for mixin in drivers[1:]
|
||||
]
|
||||
logger.trace(f"Detected driver {DriverClass} with mixins {mixins}.")
|
||||
|
@@ -9,13 +9,12 @@ FrontMatter:
|
||||
description: nonebot.config 模块
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
from ipaddress import IPv4Address
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Tuple, Union, Mapping, Optional
|
||||
|
||||
from pydantic.utils import deep_update
|
||||
from pydantic import Extra, BaseSettings, IPvAnyAddress
|
||||
from pydantic import Extra, Field, BaseSettings, IPvAnyAddress
|
||||
from pydantic.env_settings import (
|
||||
DotenvType,
|
||||
SettingsError,
|
||||
@@ -159,7 +158,7 @@ class Config(BaseConfig):
|
||||
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
|
||||
这些配置将会在 json 反序列化后一起带入 `Config` 类中。
|
||||
|
||||
配置方法参考: [配置](https://v2.nonebot.dev/docs/tutorial/configuration)
|
||||
配置方法参考: [配置](https://v2.nonebot.dev/docs/appendices/config)
|
||||
"""
|
||||
|
||||
_env_file: DotenvType = ".env", ".env.prod"
|
||||
@@ -174,7 +173,7 @@ class Config(BaseConfig):
|
||||
"""
|
||||
host: IPvAnyAddress = IPv4Address("127.0.0.1") # type: ignore
|
||||
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的 IP/主机名。"""
|
||||
port: int = 8080
|
||||
port: int = Field(default=8080, ge=1, le=65535)
|
||||
"""NoneBot {ref}`nonebot.drivers.ReverseDriver` 服务端监听的端口。"""
|
||||
log_level: Union[int, str] = "INFO"
|
||||
"""NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称
|
||||
|
@@ -4,6 +4,8 @@ FrontMatter:
|
||||
sidebar_position: 9
|
||||
description: nonebot.consts 模块
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from typing import Literal
|
||||
|
||||
# used by Matcher
|
||||
@@ -30,6 +32,8 @@ CMD_ARG_KEY: Literal["command_arg"] = "command_arg"
|
||||
"""命令参数存储 key"""
|
||||
CMD_START_KEY: Literal["command_start"] = "command_start"
|
||||
"""命令开头存储 key"""
|
||||
CMD_WHITESPACE_KEY: Literal["command_whitespace"] = "command_whitespace"
|
||||
"""命令与参数间空白符存储 key"""
|
||||
|
||||
SHELL_ARGS: Literal["_args"] = "_args"
|
||||
"""shell 命令 parse 后参数字典存储 key"""
|
||||
@@ -38,6 +42,8 @@ SHELL_ARGV: Literal["_argv"] = "_argv"
|
||||
|
||||
REGEX_MATCHED: Literal["_matched"] = "_matched"
|
||||
"""正则匹配结果存储 key"""
|
||||
REGEX_STR: Literal["_matched_str"] = "_matched_str"
|
||||
"""正则匹配文本存储 key"""
|
||||
REGEX_GROUP: Literal["_matched_groups"] = "_matched_groups"
|
||||
"""正则匹配 group 元组存储 key"""
|
||||
REGEX_DICT: Literal["_matched_dict"] = "_matched_dict"
|
||||
@@ -50,3 +56,5 @@ FULLMATCH_KEY: Literal["_fullmatch"] = "_fullmatch"
|
||||
"""响应触发完整消息 key"""
|
||||
KEYWORD_KEY: Literal["_keyword"] = "_keyword"
|
||||
"""响应触发关键字 key"""
|
||||
|
||||
WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt")
|
||||
|
@@ -1,152 +0,0 @@
|
||||
import signal
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Set, Union, Callable, Awaitable, cast
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.drivers import Driver
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
HOOK_FUNC = Union[Callable[[], None], Callable[[], Awaitable[None]]]
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
||||
)
|
||||
|
||||
|
||||
class BlockDriver(Driver):
|
||||
def __init__(self, env: Env, config: Config):
|
||||
super().__init__(env, config)
|
||||
self.startup_funcs: Set[HOOK_FUNC] = set()
|
||||
self.shutdown_funcs: Set[HOOK_FUNC] = set()
|
||||
self.should_exit: asyncio.Event = asyncio.Event()
|
||||
self.force_exit: bool = False
|
||||
|
||||
@property
|
||||
@overrides(Driver)
|
||||
def type(self) -> str:
|
||||
"""驱动名称: `block_driver`"""
|
||||
return "block_driver"
|
||||
|
||||
@property
|
||||
@overrides(Driver)
|
||||
def logger(self):
|
||||
"""block driver 使用的 logger"""
|
||||
return logger
|
||||
|
||||
@overrides(Driver)
|
||||
def on_startup(self, func: HOOK_FUNC) -> HOOK_FUNC:
|
||||
"""
|
||||
注册一个启动时执行的函数
|
||||
"""
|
||||
self.startup_funcs.add(func)
|
||||
return func
|
||||
|
||||
@overrides(Driver)
|
||||
def on_shutdown(self, func: HOOK_FUNC) -> HOOK_FUNC:
|
||||
"""
|
||||
注册一个停止时执行的函数
|
||||
"""
|
||||
self.shutdown_funcs.add(func)
|
||||
return func
|
||||
|
||||
@overrides(Driver)
|
||||
def run(self, *args, **kwargs):
|
||||
"""启动 block driver"""
|
||||
super().run(*args, **kwargs)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self.serve())
|
||||
|
||||
async def serve(self):
|
||||
self.install_signal_handlers()
|
||||
await self.startup()
|
||||
if self.should_exit.is_set():
|
||||
return
|
||||
await self.main_loop()
|
||||
await self.shutdown()
|
||||
|
||||
async def startup(self):
|
||||
# run startup
|
||||
cors = [
|
||||
cast(Callable[..., Awaitable[None]], startup)()
|
||||
if is_coroutine_callable(startup)
|
||||
else run_sync(startup)()
|
||||
for startup in self.startup_funcs
|
||||
]
|
||||
if cors:
|
||||
try:
|
||||
await asyncio.gather(*cors)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running startup function. "
|
||||
"Ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
logger.info("Application startup completed.")
|
||||
|
||||
async def main_loop(self):
|
||||
await self.should_exit.wait()
|
||||
|
||||
async def shutdown(self):
|
||||
logger.info("Shutting down")
|
||||
|
||||
logger.info("Waiting for application shutdown.")
|
||||
# run shutdown
|
||||
cors = [
|
||||
cast(Callable[..., Awaitable[None]], shutdown)()
|
||||
if is_coroutine_callable(shutdown)
|
||||
else run_sync(shutdown)()
|
||||
for shutdown in self.shutdown_funcs
|
||||
]
|
||||
if cors:
|
||||
try:
|
||||
await asyncio.gather(*cors)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running shutdown function. "
|
||||
"Ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
for task in asyncio.all_tasks():
|
||||
if task is not asyncio.current_task() and not task.done():
|
||||
task.cancel()
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
if tasks and not self.force_exit:
|
||||
logger.info("Waiting for tasks to finish. (CTRL+C to force quit)")
|
||||
while tasks and not self.force_exit:
|
||||
await asyncio.sleep(0.1)
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
logger.info("Application shutdown complete.")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.stop()
|
||||
|
||||
def install_signal_handlers(self) -> None:
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
# Signals can only be listened to from the main thread.
|
||||
return
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
for sig in HANDLED_SIGNALS:
|
||||
loop.add_signal_handler(sig, self.handle_exit, sig, None)
|
||||
except NotImplementedError:
|
||||
# Windows
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self.handle_exit)
|
||||
|
||||
def handle_exit(self, sig, frame):
|
||||
if self.should_exit.is_set():
|
||||
self.force_exit = True
|
||||
else:
|
||||
self.should_exit.set()
|
45
nonebot/drivers/_lifespan.py
Normal file
45
nonebot/drivers/_lifespan.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from typing import Any, List, Union, Callable, Awaitable, cast
|
||||
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
SYNC_LIFESPAN_FUNC = Callable[[], Any]
|
||||
ASYNC_LIFESPAN_FUNC = Callable[[], Awaitable[Any]]
|
||||
LIFESPAN_FUNC = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
||||
|
||||
|
||||
class Lifespan:
|
||||
def __init__(self) -> None:
|
||||
self._startup_funcs: List[LIFESPAN_FUNC] = []
|
||||
self._shutdown_funcs: List[LIFESPAN_FUNC] = []
|
||||
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
self._startup_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
self._shutdown_funcs.append(func)
|
||||
return func
|
||||
|
||||
@staticmethod
|
||||
async def _run_lifespan_func(
|
||||
funcs: List[LIFESPAN_FUNC],
|
||||
) -> None:
|
||||
for func in funcs:
|
||||
if is_coroutine_callable(func):
|
||||
await cast(ASYNC_LIFESPAN_FUNC, func)()
|
||||
else:
|
||||
await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()
|
||||
|
||||
async def startup(self) -> None:
|
||||
if self._startup_funcs:
|
||||
await self._run_lifespan_func(self._startup_funcs)
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
if self._shutdown_funcs:
|
||||
await self._run_lifespan_func(self._shutdown_funcs)
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self.startup()
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
await self.shutdown()
|
@@ -21,16 +21,16 @@ from contextlib import asynccontextmanager
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.drivers import Request, Response
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers._block_driver import BlockDriver
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import HTTPVersion, ForwardMixin, ForwardDriver, combine_driver
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError: # pragma: no cover
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install aiohttp first to use this driver. `pip install nonebot2[aiohttp]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
|
||||
class Mixin(ForwardMixin):
|
||||
@@ -56,7 +56,13 @@ class Mixin(ForwardMixin):
|
||||
files = aiohttp.FormData()
|
||||
for name, file in setup.files:
|
||||
files.add_field(name, file[1], content_type=file[2], filename=file[0])
|
||||
async with aiohttp.ClientSession(version=version, trust_env=True) as session:
|
||||
|
||||
cookies = {
|
||||
cookie.name: cookie.value for cookie in setup.cookies if cookie.value
|
||||
}
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, version=version, trust_env=True
|
||||
) as session:
|
||||
async with session.request(
|
||||
setup.method,
|
||||
setup.url,
|
||||
@@ -66,13 +72,12 @@ class Mixin(ForwardMixin):
|
||||
timeout=timeout,
|
||||
proxy=setup.proxy,
|
||||
) as response:
|
||||
res = Response(
|
||||
return Response(
|
||||
response.status,
|
||||
headers=response.headers.copy(),
|
||||
content=await response.read(),
|
||||
request=setup,
|
||||
)
|
||||
return res
|
||||
|
||||
@overrides(ForwardMixin)
|
||||
@asynccontextmanager
|
||||
@@ -92,8 +97,7 @@ class Mixin(ForwardMixin):
|
||||
headers=setup.headers,
|
||||
proxy=setup.proxy,
|
||||
) as ws:
|
||||
websocket = WebSocket(request=setup, session=session, websocket=ws)
|
||||
yield websocket
|
||||
yield WebSocket(request=setup, session=session, websocket=ws)
|
||||
|
||||
|
||||
class WebSocket(BaseWebSocket):
|
||||
@@ -166,5 +170,5 @@ class WebSocket(BaseWebSocket):
|
||||
await self.websocket.send_bytes(data)
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
"""AIOHTTP Driver"""
|
||||
|
@@ -1,5 +1,11 @@
|
||||
"""[FastAPI](https://fastapi.tiangolo.com/) 驱动适配
|
||||
|
||||
```bash
|
||||
nb driver install fastapi
|
||||
# 或者
|
||||
pip install nonebot2[fastapi]
|
||||
```
|
||||
|
||||
:::tip 提示
|
||||
本驱动仅支持服务端连接
|
||||
:::
|
||||
@@ -13,13 +19,9 @@ FrontMatter:
|
||||
import logging
|
||||
import contextlib
|
||||
from functools import wraps
|
||||
from typing import Any, List, Tuple, Union, Callable, Optional
|
||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||
|
||||
import uvicorn
|
||||
from pydantic import BaseSettings
|
||||
from fastapi.responses import Response
|
||||
from fastapi import FastAPI, Request, UploadFile, status
|
||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
||||
|
||||
from nonebot.config import Env
|
||||
from nonebot.typing import overrides
|
||||
@@ -30,6 +32,18 @@ from nonebot.drivers import Request as BaseRequest
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
||||
|
||||
try:
|
||||
import uvicorn
|
||||
from fastapi.responses import Response
|
||||
from fastapi import FastAPI, Request, UploadFile, status
|
||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install FastAPI by using `pip install nonebot2[fastapi]`"
|
||||
) from e
|
||||
|
||||
|
||||
def catch_closed(func):
|
||||
@wraps(func)
|
||||
@@ -65,6 +79,8 @@ class Config(BaseSettings):
|
||||
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
fastapi_reload_excludes: Optional[List[str]] = None
|
||||
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
fastapi_extra: Dict[str, Any] = {}
|
||||
"""传递给 `FastAPI` 的其他参数。"""
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
@@ -78,10 +94,14 @@ class Driver(ReverseDriver):
|
||||
|
||||
self.fastapi_config: Config = Config(**config.dict())
|
||||
|
||||
self._lifespan = Lifespan()
|
||||
|
||||
self._server_app = FastAPI(
|
||||
lifespan=self._lifespan_manager,
|
||||
openapi_url=self.fastapi_config.fastapi_openapi_url,
|
||||
docs_url=self.fastapi_config.fastapi_docs_url,
|
||||
redoc_url=self.fastapi_config.fastapi_redoc_url,
|
||||
**self.fastapi_config.fastapi_extra,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -133,14 +153,20 @@ class Driver(ReverseDriver):
|
||||
)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
def on_startup(self, func: Callable) -> Callable:
|
||||
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#startup-event>`_"""
|
||||
return self.server_app.on_event("startup")(func)
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
return self._lifespan.on_startup(func)
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
def on_shutdown(self, func: Callable) -> Callable:
|
||||
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#shutdown-event>`_"""
|
||||
return self.server_app.on_event("shutdown")(func)
|
||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
return self._lifespan.on_shutdown(func)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def _lifespan_manager(self, app: FastAPI):
|
||||
await self._lifespan.startup()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await self._lifespan.shutdown()
|
||||
|
||||
@overrides(ReverseDriver)
|
||||
def run(
|
||||
|
@@ -18,7 +18,7 @@ from typing import Type, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.drivers._block_driver import BlockDriver
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import (
|
||||
Request,
|
||||
Response,
|
||||
@@ -31,10 +31,10 @@ from nonebot.drivers import (
|
||||
|
||||
try:
|
||||
import httpx
|
||||
except ImportError: # pragma: no cover
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install httpx by using `pip install nonebot2[httpx]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
|
||||
class Mixin(ForwardMixin):
|
||||
@@ -48,17 +48,18 @@ class Mixin(ForwardMixin):
|
||||
@overrides(ForwardMixin)
|
||||
async def request(self, setup: Request) -> Response:
|
||||
async with httpx.AsyncClient(
|
||||
cookies=setup.cookies.jar,
|
||||
http2=setup.version == HTTPVersion.H2,
|
||||
proxies=setup.proxy, # type: ignore
|
||||
proxies=setup.proxy,
|
||||
follow_redirects=True,
|
||||
) as client:
|
||||
response = await client.request(
|
||||
setup.method,
|
||||
str(setup.url),
|
||||
content=setup.content, # type: ignore
|
||||
data=setup.data, # type: ignore
|
||||
content=setup.content,
|
||||
data=setup.data,
|
||||
json=setup.json,
|
||||
files=setup.files, # type: ignore
|
||||
files=setup.files,
|
||||
headers=tuple(setup.headers.items()),
|
||||
timeout=setup.timeout,
|
||||
)
|
||||
@@ -76,5 +77,5 @@ class Mixin(ForwardMixin):
|
||||
yield ws
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
"""HTTPX Driver"""
|
||||
|
152
nonebot/drivers/none.py
Normal file
152
nonebot/drivers/none.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""None 驱动适配
|
||||
|
||||
:::tip 提示
|
||||
本驱动不支持任何服务器或客户端连接
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
sidebar_position: 6
|
||||
description: nonebot.drivers.none 模块
|
||||
"""
|
||||
|
||||
|
||||
import signal
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.consts import WINDOWS
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
|
||||
from ._lifespan import LIFESPAN_FUNC, Lifespan
|
||||
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
||||
)
|
||||
if WINDOWS: # pragma: py-win32
|
||||
HANDLED_SIGNALS += (signal.SIGBREAK,) # Windows signal 21. Sent by Ctrl+Break.
|
||||
|
||||
|
||||
class Driver(BaseDriver):
|
||||
"""None 驱动框架"""
|
||||
|
||||
def __init__(self, env: Env, config: Config):
|
||||
super().__init__(env, config)
|
||||
|
||||
self._lifespan = Lifespan()
|
||||
|
||||
self.should_exit: asyncio.Event = asyncio.Event()
|
||||
self.force_exit: bool = False
|
||||
|
||||
@property
|
||||
@overrides(BaseDriver)
|
||||
def type(self) -> str:
|
||||
"""驱动名称: `none`"""
|
||||
return "none"
|
||||
|
||||
@property
|
||||
@overrides(BaseDriver)
|
||||
def logger(self):
|
||||
"""none driver 使用的 logger"""
|
||||
return logger
|
||||
|
||||
@overrides(BaseDriver)
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
注册一个启动时执行的函数
|
||||
"""
|
||||
return self._lifespan.on_startup(func)
|
||||
|
||||
@overrides(BaseDriver)
|
||||
def on_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
注册一个停止时执行的函数
|
||||
"""
|
||||
return self._lifespan.on_shutdown(func)
|
||||
|
||||
@overrides(BaseDriver)
|
||||
def run(self, *args, **kwargs):
|
||||
"""启动 none driver"""
|
||||
super().run(*args, **kwargs)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self._serve())
|
||||
|
||||
async def _serve(self):
|
||||
self._install_signal_handlers()
|
||||
await self._startup()
|
||||
if self.should_exit.is_set():
|
||||
return
|
||||
await self._main_loop()
|
||||
await self._shutdown()
|
||||
|
||||
async def _startup(self):
|
||||
try:
|
||||
await self._lifespan.startup()
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running startup function. "
|
||||
"Ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
logger.info("Application startup completed.")
|
||||
|
||||
async def _main_loop(self):
|
||||
await self.should_exit.wait()
|
||||
|
||||
async def _shutdown(self):
|
||||
logger.info("Shutting down")
|
||||
|
||||
logger.info("Waiting for application shutdown.")
|
||||
|
||||
try:
|
||||
await self._lifespan.shutdown()
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running shutdown function. "
|
||||
"Ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
for task in asyncio.all_tasks():
|
||||
if task is not asyncio.current_task() and not task.done():
|
||||
task.cancel()
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
if tasks and not self.force_exit:
|
||||
logger.info("Waiting for tasks to finish. (CTRL+C to force quit)")
|
||||
while tasks and not self.force_exit:
|
||||
await asyncio.sleep(0.1)
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
logger.info("Application shutdown complete.")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.stop()
|
||||
|
||||
def _install_signal_handlers(self) -> None:
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
# Signals can only be listened to from the main thread.
|
||||
return
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
for sig in HANDLED_SIGNALS:
|
||||
loop.add_signal_handler(sig, self._handle_exit, sig, None)
|
||||
except NotImplementedError:
|
||||
# Windows
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self._handle_exit)
|
||||
|
||||
def _handle_exit(self, sig, frame):
|
||||
if self.should_exit.is_set():
|
||||
self.force_exit = True
|
||||
else:
|
||||
self.should_exit.set()
|
@@ -17,9 +17,8 @@ FrontMatter:
|
||||
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from typing import List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
|
||||
from typing import Any, Dict, List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
|
||||
|
||||
import uvicorn
|
||||
from pydantic import BaseSettings
|
||||
|
||||
from nonebot.config import Env
|
||||
@@ -32,15 +31,16 @@ from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
try:
|
||||
import uvicorn
|
||||
from quart import request as _request
|
||||
from quart import websocket as _websocket
|
||||
from quart import Quart, Request, Response
|
||||
from quart.datastructures import FileStorage
|
||||
from quart import Websocket as QuartWebSocket
|
||||
except ImportError: # pragma: no cover
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install Quart by using `pip install nonebot2[quart]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
|
||||
|
||||
@@ -69,6 +69,8 @@ class Config(BaseSettings):
|
||||
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
quart_reload_excludes: Optional[List[str]] = None
|
||||
"""不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
quart_extra: Dict[str, Any] = {}
|
||||
"""传递给 `Quart` 的其他参数。"""
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
@@ -82,7 +84,9 @@ class Driver(ReverseDriver):
|
||||
|
||||
self.quart_config = Config(**config.dict())
|
||||
|
||||
self._server_app = Quart(self.__class__.__qualname__)
|
||||
self._server_app = Quart(
|
||||
self.__class__.__qualname__, **self.quart_config.quart_extra
|
||||
)
|
||||
|
||||
@property
|
||||
@overrides(ReverseDriver)
|
||||
|
@@ -23,17 +23,17 @@ from nonebot.typing import overrides
|
||||
from nonebot.log import LoguruHandler
|
||||
from nonebot.drivers import Request, Response
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers._block_driver import BlockDriver
|
||||
from nonebot.drivers.none import Driver as NoneDriver
|
||||
from nonebot.drivers import WebSocket as BaseWebSocket
|
||||
from nonebot.drivers import ForwardMixin, ForwardDriver, combine_driver
|
||||
|
||||
try:
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
from websockets.legacy.client import Connect, WebSocketClientProtocol
|
||||
except ImportError: # pragma: no cover
|
||||
except ModuleNotFoundError as e: # pragma: no cover
|
||||
raise ImportError(
|
||||
"Please install websockets by using `pip install nonebot2[websockets]`"
|
||||
) from None
|
||||
) from e
|
||||
|
||||
logger = logging.Logger("websockets.client", "INFO")
|
||||
logger.addHandler(LoguruHandler())
|
||||
@@ -70,7 +70,7 @@ class Mixin(ForwardMixin):
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||
connection = Connect(
|
||||
str(setup.url),
|
||||
extra_headers=setup.headers.items(),
|
||||
extra_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
||||
open_timeout=setup.timeout,
|
||||
)
|
||||
async with connection as ws:
|
||||
@@ -101,8 +101,7 @@ class WebSocket(BaseWebSocket):
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
msg = await self.websocket.recv()
|
||||
return msg
|
||||
return await self.websocket.recv()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
@@ -129,5 +128,5 @@ class WebSocket(BaseWebSocket):
|
||||
await self.websocket.send(data)
|
||||
|
||||
|
||||
Driver: Type[ForwardDriver] = combine_driver(BlockDriver, Mixin) # type: ignore
|
||||
Driver: Type[ForwardDriver] = combine_driver(NoneDriver, Mixin) # type: ignore
|
||||
"""Websockets Driver"""
|
||||
|
@@ -37,6 +37,9 @@ from pydantic.fields import ModelField
|
||||
class NoneBotException(Exception):
|
||||
"""所有 NoneBot 发生的异常基类。"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
# Rule Exception
|
||||
class ParserExit(NoneBotException):
|
||||
@@ -53,9 +56,6 @@ class ParserExit(NoneBotException):
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
# Processor Exception
|
||||
class ProcessException(NoneBotException):
|
||||
@@ -75,9 +75,6 @@ class IgnoredException(ProcessException):
|
||||
def __repr__(self) -> str:
|
||||
return f"IgnoredException(reason={self.reason!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class SkippedException(ProcessException):
|
||||
"""指示 NoneBot 立即结束当前 `Dependent` 的运行。
|
||||
@@ -109,9 +106,6 @@ class TypeMisMatch(SkippedException):
|
||||
f"type={self.param._type_display()}, value={self.value!r}>"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class MockApiException(ProcessException):
|
||||
"""指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。可由 api hook 抛出。
|
||||
@@ -126,9 +120,6 @@ class MockApiException(ProcessException):
|
||||
def __repr__(self) -> str:
|
||||
return f"MockApiException(result={self.result!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class StopPropagation(ProcessException):
|
||||
"""指示 NoneBot 终止事件向下层传播。
|
||||
@@ -244,6 +235,3 @@ class WebSocketClosed(DriverException):
|
||||
+ (f", reason={self.reason!r}" if self.reason else "")
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
@@ -66,8 +66,9 @@ class Adapter(abc.ABC):
|
||||
参数:
|
||||
bot: {ref}`nonebot.adapters.Bot` 实例
|
||||
"""
|
||||
if self.bots.pop(bot.self_id, None) is None:
|
||||
raise RuntimeError(f"{bot} not found in adapter {self.get_name()}")
|
||||
self.driver._bot_disconnect(bot)
|
||||
self.bots.pop(bot.self_id, None)
|
||||
|
||||
def setup_http_server(self, setup: HTTPServerSetup):
|
||||
"""设置一个 HTTP 服务器路由配置"""
|
||||
|
@@ -47,7 +47,7 @@ class Event(abc.ABC, BaseModel):
|
||||
通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时,可以抛出 `NoLogException` 异常。
|
||||
|
||||
异常:
|
||||
NoLogException
|
||||
NoLogException:
|
||||
"""
|
||||
return f"[{self.get_event_name()}]: {self.get_event_description()}"
|
||||
|
||||
|
@@ -49,11 +49,11 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def __init__( # type:ignore
|
||||
self, template, factory=str
|
||||
) -> None: # TODO: fix type hint here
|
||||
self.template: TF = template
|
||||
self.factory: Type[TF] = factory
|
||||
def __init__(
|
||||
self, template: Union[str, TM], factory: Union[Type[str], Type[TM]] = str
|
||||
) -> None:
|
||||
self.template: TF = template # type: ignore
|
||||
self.factory: Type[TF] = factory # type: ignore
|
||||
self.format_specs: Dict[str, FormatSpecFunc] = {}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -98,7 +98,7 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
else:
|
||||
raise TypeError("template must be a string or instance of Message!")
|
||||
|
||||
self.check_unused_args(list(used_args), args, kwargs)
|
||||
self.check_unused_args(used_args, args, kwargs)
|
||||
return cast(TF, full_message)
|
||||
|
||||
def vformat(
|
||||
@@ -119,10 +119,9 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
) -> Tuple[TF, int]:
|
||||
results: List[Any] = [self.factory()]
|
||||
|
||||
for (literal_text, field_name, format_spec, conversion) in self.parse(
|
||||
for literal_text, field_name, format_spec, conversion in self.parse(
|
||||
format_string
|
||||
):
|
||||
|
||||
# output the literal text
|
||||
if literal_text:
|
||||
results.append(literal_text)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import abc
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
|
||||
|
||||
from nonebot.log import logger
|
||||
@@ -8,8 +8,12 @@ from nonebot.config import Env, Config
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.utils import escape_tag, run_coro_with_catch
|
||||
from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook
|
||||
from nonebot.internal.params import BotParam, DependParam, DefaultParam
|
||||
from nonebot.typing import (
|
||||
T_DependencyCache,
|
||||
T_BotConnectionHook,
|
||||
T_BotDisconnectionHook,
|
||||
)
|
||||
|
||||
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
|
||||
|
||||
@@ -135,20 +139,22 @@ class Driver(abc.ABC):
|
||||
self._bots[bot.self_id] = bot
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
|
||||
self._bot_connection_hook,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
async with AsyncExitStack() as stack:
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
||||
(SkippedException,),
|
||||
)
|
||||
for hook in self._bot_connection_hook
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
asyncio.create_task(_run_hook(bot))
|
||||
|
||||
@@ -158,20 +164,22 @@ class Driver(abc.ABC):
|
||||
del self._bots[bot.self_id]
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
|
||||
self._bot_disconnection_hook,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
async with AsyncExitStack() as stack:
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
||||
(SkippedException,),
|
||||
)
|
||||
for hook in self._bot_disconnection_hook
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
asyncio.create_task(_run_hook(bot))
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import abc
|
||||
import urllib.request
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from http.cookiejar import Cookie, CookieJar
|
||||
@@ -105,12 +106,9 @@ class Request:
|
||||
self.url: URL = url
|
||||
|
||||
# headers
|
||||
self.headers: CIMultiDict[str]
|
||||
if headers is not None:
|
||||
self.headers = CIMultiDict(headers)
|
||||
else:
|
||||
self.headers = CIMultiDict()
|
||||
|
||||
self.headers: CIMultiDict[str] = (
|
||||
CIMultiDict(headers) if headers is not None else CIMultiDict()
|
||||
)
|
||||
# cookies
|
||||
self.cookies = Cookies(cookies)
|
||||
|
||||
@@ -147,12 +145,9 @@ class Response:
|
||||
self.status_code: int = status_code
|
||||
|
||||
# headers
|
||||
self.headers: CIMultiDict[str]
|
||||
if headers is not None:
|
||||
self.headers = CIMultiDict(headers)
|
||||
else:
|
||||
self.headers = CIMultiDict()
|
||||
|
||||
self.headers: CIMultiDict[str] = (
|
||||
CIMultiDict(headers) if headers is not None else CIMultiDict()
|
||||
)
|
||||
# body
|
||||
self.content: ContentTypes = content
|
||||
|
||||
@@ -308,6 +303,11 @@ class Cookies(MutableMapping):
|
||||
for cookie in cookies.jar:
|
||||
self.jar.set_cookie(cookie)
|
||||
|
||||
def as_header(self, request: Request) -> Dict[str, str]:
|
||||
urllib_request = self._CookieCompatRequest(request)
|
||||
self.jar.add_cookie_header(urllib_request)
|
||||
return urllib_request.added_headers
|
||||
|
||||
def __setitem__(self, name: str, value: str) -> None:
|
||||
return self.set(name, value)
|
||||
|
||||
@@ -333,6 +333,20 @@ class Cookies(MutableMapping):
|
||||
)
|
||||
return f"{self.__class__.__name__}({cookies_repr})"
|
||||
|
||||
class _CookieCompatRequest(urllib.request.Request):
|
||||
def __init__(self, request: Request) -> None:
|
||||
super().__init__(
|
||||
url=str(request.url),
|
||||
headers=dict(request.headers),
|
||||
method=request.method,
|
||||
)
|
||||
self.request = request
|
||||
self.added_headers: Dict[str, str] = {}
|
||||
|
||||
def add_unredirected_header(self, key: str, value: str) -> None:
|
||||
super().add_unredirected_header(key, value)
|
||||
self.added_headers[key] = value
|
||||
|
||||
|
||||
@dataclass
|
||||
class HTTPServerSetup:
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from types import ModuleType
|
||||
from contextvars import ContextVar
|
||||
from typing_extensions import Self
|
||||
from datetime import datetime, timedelta
|
||||
from contextlib import AsyncExitStack, contextmanager
|
||||
from typing import (
|
||||
@@ -10,6 +11,7 @@ from typing import (
|
||||
Union,
|
||||
TypeVar,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
NoReturn,
|
||||
Optional,
|
||||
@@ -19,7 +21,7 @@ from typing import (
|
||||
from nonebot.log import logger
|
||||
from nonebot.internal.rule import Rule
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.internal.permission import USER, User, Permission
|
||||
from nonebot.internal.permission import User, Permission
|
||||
from nonebot.internal.adapter import (
|
||||
Bot,
|
||||
Event,
|
||||
@@ -80,7 +82,7 @@ class MatcherMeta(type):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Matcher(type={self.type!r}"
|
||||
f"{self.__name__}(type={self.type!r}"
|
||||
+ (f", module={self.module_name}" if self.module_name else "")
|
||||
+ ")"
|
||||
)
|
||||
@@ -89,38 +91,38 @@ class MatcherMeta(type):
|
||||
class Matcher(metaclass=MatcherMeta):
|
||||
"""事件响应器类"""
|
||||
|
||||
plugin: Optional["Plugin"] = None
|
||||
plugin: ClassVar[Optional["Plugin"]] = None
|
||||
"""事件响应器所在插件"""
|
||||
module: Optional[ModuleType] = None
|
||||
module: ClassVar[Optional[ModuleType]] = None
|
||||
"""事件响应器所在插件模块"""
|
||||
plugin_name: Optional[str] = None
|
||||
plugin_name: ClassVar[Optional[str]] = None
|
||||
"""事件响应器所在插件名"""
|
||||
module_name: Optional[str] = None
|
||||
module_name: ClassVar[Optional[str]] = None
|
||||
"""事件响应器所在点分割插件模块路径"""
|
||||
|
||||
type: str = ""
|
||||
type: ClassVar[str] = ""
|
||||
"""事件响应器类型"""
|
||||
rule: Rule = Rule()
|
||||
rule: ClassVar[Rule] = Rule()
|
||||
"""事件响应器匹配规则"""
|
||||
permission: Permission = Permission()
|
||||
permission: ClassVar[Permission] = Permission()
|
||||
"""事件响应器触发权限"""
|
||||
handlers: List[Dependent[Any]] = []
|
||||
"""事件响应器拥有的事件处理函数列表"""
|
||||
priority: int = 1
|
||||
priority: ClassVar[int] = 1
|
||||
"""事件响应器优先级"""
|
||||
block: bool = False
|
||||
"""事件响应器是否阻止事件传播"""
|
||||
temp: bool = False
|
||||
temp: ClassVar[bool] = False
|
||||
"""事件响应器是否为临时"""
|
||||
expire_time: Optional[datetime] = None
|
||||
expire_time: ClassVar[Optional[datetime]] = None
|
||||
"""事件响应器过期时间点"""
|
||||
|
||||
_default_state: T_State = {}
|
||||
_default_state: ClassVar[T_State] = {}
|
||||
"""事件响应器默认状态"""
|
||||
|
||||
_default_type_updater: Optional[Dependent[str]] = None
|
||||
_default_type_updater: ClassVar[Optional[Dependent[str]]] = None
|
||||
"""事件响应器类型更新函数"""
|
||||
_default_permission_updater: Optional[Dependent[Permission]] = None
|
||||
_default_permission_updater: ClassVar[Optional[Dependent[Permission]]] = None
|
||||
"""事件响应器权限更新函数"""
|
||||
|
||||
HANDLER_PARAM_TYPES = (
|
||||
@@ -139,7 +141,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Matcher(type={self.type!r}"
|
||||
f"{self.__class__.__name__}(type={self.type!r}"
|
||||
+ (f", module={self.module_name}" if self.module_name else "")
|
||||
+ ")"
|
||||
)
|
||||
@@ -163,7 +165,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
default_permission_updater: Optional[
|
||||
Union[T_PermissionUpdater, Dependent[Permission]]
|
||||
] = None,
|
||||
) -> Type["Matcher"]:
|
||||
) -> Type[Self]:
|
||||
"""
|
||||
创建一个新的事件响应器,并存储至 `matchers <#matchers>`_
|
||||
|
||||
@@ -184,8 +186,8 @@ class Matcher(metaclass=MatcherMeta):
|
||||
Type[Matcher]: 新的事件响应器类
|
||||
"""
|
||||
NewMatcher = type(
|
||||
"Matcher",
|
||||
(Matcher,),
|
||||
cls.__name__,
|
||||
(cls,),
|
||||
{
|
||||
"plugin": plugin,
|
||||
"module": module,
|
||||
@@ -247,6 +249,11 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
return NewMatcher
|
||||
|
||||
@classmethod
|
||||
def destroy(cls) -> None:
|
||||
"""销毁当前的事件响应器"""
|
||||
matchers[cls.priority].remove(cls)
|
||||
|
||||
@classmethod
|
||||
async def check_perm(
|
||||
cls,
|
||||
@@ -372,7 +379,6 @@ class Matcher(metaclass=MatcherMeta):
|
||||
_parameterless = (Depends(_receive), *(parameterless or tuple()))
|
||||
|
||||
def _decorator(func: T_Handler) -> T_Handler:
|
||||
|
||||
if cls.handlers and cls.handlers[-1].call is func:
|
||||
func_handler = cls.handlers[-1]
|
||||
new_handler = Dependent(
|
||||
@@ -420,7 +426,6 @@ class Matcher(metaclass=MatcherMeta):
|
||||
_parameterless = (Depends(_key_getter), *(parameterless or tuple()))
|
||||
|
||||
def _decorator(func: T_Handler) -> T_Handler:
|
||||
|
||||
if cls.handlers and cls.handlers[-1].call is func:
|
||||
func_handler = cls.handlers[-1]
|
||||
new_handler = Dependent(
|
||||
@@ -640,23 +645,44 @@ class Matcher(metaclass=MatcherMeta):
|
||||
"""阻止事件传播"""
|
||||
self.block = True
|
||||
|
||||
async def update_type(self, bot: Bot, event: Event) -> str:
|
||||
async def update_type(
|
||||
self,
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> str:
|
||||
updater = self.__class__._default_type_updater
|
||||
return (
|
||||
await updater(bot=bot, event=event, state=self.state, matcher=self)
|
||||
await updater(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
matcher=self,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
if updater
|
||||
else "message"
|
||||
)
|
||||
|
||||
async def update_permission(self, bot: Bot, event: Event) -> Permission:
|
||||
async def update_permission(
|
||||
self,
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> Permission:
|
||||
if updater := self.__class__._default_permission_updater:
|
||||
return await updater(bot=bot, event=event, state=self.state, matcher=self)
|
||||
permission = self.permission
|
||||
if len(permission.checkers) == 1 and isinstance(
|
||||
user_perm := tuple(permission.checkers)[0].call, User
|
||||
):
|
||||
permission = user_perm.perm
|
||||
return USER(event.get_session_id(), perm=permission)
|
||||
return await updater(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=self.state,
|
||||
matcher=self,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
return Permission(User.from_event(event, perm=self.permission))
|
||||
|
||||
async def resolve_reject(self):
|
||||
handler = current_handler.get()
|
||||
@@ -728,10 +754,12 @@ class Matcher(metaclass=MatcherMeta):
|
||||
|
||||
except RejectedException:
|
||||
await self.resolve_reject()
|
||||
type_ = await self.update_type(bot, event)
|
||||
permission = await self.update_permission(bot, event)
|
||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||
permission = await self.update_permission(
|
||||
bot, event, stack, dependency_cache
|
||||
)
|
||||
|
||||
Matcher.new(
|
||||
self.new(
|
||||
type_,
|
||||
Rule(),
|
||||
permission,
|
||||
@@ -747,10 +775,12 @@ class Matcher(metaclass=MatcherMeta):
|
||||
default_permission_updater=self.__class__._default_permission_updater,
|
||||
)
|
||||
except PausedException:
|
||||
type_ = await self.update_type(bot, event)
|
||||
permission = await self.update_permission(bot, event)
|
||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||
permission = await self.update_permission(
|
||||
bot, event, stack, dependency_cache
|
||||
)
|
||||
|
||||
Matcher.new(
|
||||
self.new(
|
||||
type_,
|
||||
Rule(),
|
||||
permission,
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
from typing_extensions import Annotated
|
||||
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Type, Tuple, Literal, Callable, Optional, cast
|
||||
|
||||
from pydantic.typing import get_args, get_origin
|
||||
from pydantic.fields import Required, Undefined, ModelField
|
||||
|
||||
from nonebot.dependencies.utils import check_field_type
|
||||
@@ -78,21 +80,33 @@ class DependParam(Param):
|
||||
def _check_param(
|
||||
cls, param: inspect.Parameter, allow_types: Tuple[Type[Param], ...]
|
||||
) -> Optional["DependParam"]:
|
||||
if isinstance(param.default, DependsInner):
|
||||
dependency: T_Handler
|
||||
if param.default.dependency is None:
|
||||
assert param.annotation is not param.empty, "Dependency cannot be empty"
|
||||
dependency = param.annotation
|
||||
else:
|
||||
dependency = param.default.dependency
|
||||
sub_dependent = Dependent[Any].parse(
|
||||
call=dependency,
|
||||
allow_types=allow_types,
|
||||
)
|
||||
return cls(
|
||||
Required, use_cache=param.default.use_cache, dependent=sub_dependent
|
||||
type_annotation, depends_inner = param.annotation, None
|
||||
if get_origin(param.annotation) is Annotated:
|
||||
type_annotation, *extra_args = get_args(param.annotation)
|
||||
depends_inner = next(
|
||||
(x for x in extra_args if isinstance(x, DependsInner)), None
|
||||
)
|
||||
|
||||
depends_inner = (
|
||||
param.default if isinstance(param.default, DependsInner) else depends_inner
|
||||
)
|
||||
if depends_inner is None:
|
||||
return
|
||||
|
||||
dependency: T_Handler
|
||||
if depends_inner.dependency is None:
|
||||
assert (
|
||||
type_annotation is not inspect.Signature.empty
|
||||
), "Dependency cannot be empty"
|
||||
dependency = type_annotation
|
||||
else:
|
||||
dependency = depends_inner.dependency
|
||||
sub_dependent = Dependent[Any].parse(
|
||||
call=dependency,
|
||||
allow_types=allow_types,
|
||||
)
|
||||
return cls(Required, use_cache=depends_inner.use_cache, dependent=sub_dependent)
|
||||
|
||||
@classmethod
|
||||
def _check_parameterless(
|
||||
cls, value: Any, allow_types: Tuple[Type[Param], ...]
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from typing_extensions import Self
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Set, Tuple, Union, NoReturn, Optional
|
||||
|
||||
@@ -57,7 +58,7 @@ class Permission:
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
) -> bool:
|
||||
"""检查是否满足某个权限
|
||||
"""检查是否满足某个权限。
|
||||
|
||||
参数:
|
||||
bot: Bot 对象
|
||||
@@ -109,7 +110,7 @@ class Permission:
|
||||
|
||||
|
||||
class User:
|
||||
"""检查当前事件是否属于指定会话
|
||||
"""检查当前事件是否属于指定会话。
|
||||
|
||||
参数:
|
||||
users: 会话 ID 元组
|
||||
@@ -140,13 +141,47 @@ class User:
|
||||
session in self.users and (self.perm is None or await self.perm(bot, event))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _clean_permission(cls, perm: Permission) -> Optional[Permission]:
|
||||
if len(perm.checkers) == 1 and isinstance(
|
||||
user_perm := tuple(perm.checkers)[0].call, cls
|
||||
):
|
||||
return user_perm.perm
|
||||
return perm
|
||||
|
||||
@classmethod
|
||||
def from_event(cls, event: Event, perm: Optional[Permission] = None) -> Self:
|
||||
"""从事件中获取会话 ID。
|
||||
|
||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
|
||||
|
||||
参数:
|
||||
event: Event 对象
|
||||
perm: 需同时满足的权限
|
||||
"""
|
||||
return cls((event.get_session_id(),), perm=perm and cls._clean_permission(perm))
|
||||
|
||||
@classmethod
|
||||
def from_permission(cls, *users: str, perm: Optional[Permission] = None) -> Self:
|
||||
"""指定会话与权限。
|
||||
|
||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。
|
||||
|
||||
参数:
|
||||
users: 会话白名单
|
||||
perm: 需同时满足的权限
|
||||
"""
|
||||
return cls(users, perm=perm and cls._clean_permission(perm))
|
||||
|
||||
|
||||
def USER(*users: str, perm: Optional[Permission] = None):
|
||||
"""匹配当前事件属于指定会话
|
||||
"""匹配当前事件属于指定会话。
|
||||
|
||||
如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。
|
||||
|
||||
参数:
|
||||
user: 会话白名单
|
||||
perm: 需要同时满足的权限
|
||||
"""
|
||||
|
||||
return Permission(User(users, perm))
|
||||
return Permission(User.from_permission(*users, perm=perm))
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
|
||||
|
||||
自定义 logger 请参考 [自定义日志](https://v2.nonebot.dev/docs/tutorial/custom-logger)
|
||||
自定义 logger 请参考 [自定义日志](https://v2.nonebot.dev/docs/appendices/log)
|
||||
以及 [`loguru`][loguru] 文档。
|
||||
|
||||
[loguru]: https://github.com/Delgan/loguru
|
||||
|
@@ -112,7 +112,6 @@ def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
|
||||
|
||||
|
||||
async def _check_matcher(
|
||||
priority: int,
|
||||
Matcher: Type[Matcher],
|
||||
bot: "Bot",
|
||||
event: "Event",
|
||||
@@ -122,7 +121,7 @@ async def _check_matcher(
|
||||
) -> None:
|
||||
if Matcher.expire_time and datetime.now() > Matcher.expire_time:
|
||||
with contextlib.suppress(Exception):
|
||||
matchers[priority].remove(Matcher)
|
||||
Matcher.destroy()
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -138,7 +137,7 @@ async def _check_matcher(
|
||||
|
||||
if Matcher.temp:
|
||||
with contextlib.suppress(Exception):
|
||||
matchers[priority].remove(Matcher)
|
||||
Matcher.destroy()
|
||||
await _run_matcher(Matcher, bot, event, state, stack, dependency_cache)
|
||||
|
||||
|
||||
@@ -294,7 +293,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
|
||||
pending_tasks = [
|
||||
_check_matcher(
|
||||
priority, matcher, bot, event, state.copy(), stack, dependency_cache
|
||||
matcher, bot, event, state.copy(), stack, dependency_cache
|
||||
)
|
||||
for matcher in matchers[priority]
|
||||
]
|
||||
@@ -312,6 +311,9 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
if show_log:
|
||||
logger.debug("Checking for matchers completed")
|
||||
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
proc(
|
||||
|
@@ -5,6 +5,7 @@ FrontMatter:
|
||||
description: nonebot.params 模块
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||
|
||||
from nonebot.typing import T_State
|
||||
@@ -24,6 +25,7 @@ from nonebot.internal.params import MatcherParam as MatcherParam
|
||||
from nonebot.internal.params import ExceptionParam as ExceptionParam
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
REGEX_STR,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
@@ -37,6 +39,7 @@ from nonebot.consts import (
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
|
||||
|
||||
@@ -112,6 +115,15 @@ def CommandStart() -> str:
|
||||
return Depends(_command_start)
|
||||
|
||||
|
||||
def _command_whitespace(state: T_State) -> str:
|
||||
return state[PREFIX_KEY][CMD_WHITESPACE_KEY]
|
||||
|
||||
|
||||
def CommandWhitespace() -> str:
|
||||
"""消息命令与参数之间的空白"""
|
||||
return Depends(_command_whitespace)
|
||||
|
||||
|
||||
def _shell_command_args(state: T_State) -> Any:
|
||||
return state[SHELL_ARGS] # Namespace or ParserExit
|
||||
|
||||
@@ -136,10 +148,25 @@ def _regex_matched(state: T_State) -> str:
|
||||
|
||||
def RegexMatched() -> str:
|
||||
"""正则匹配结果"""
|
||||
warnings.warn(
|
||||
'"RegexMatched()" will be changed to "re.Match" object, '
|
||||
'use "RegexStr()" instead. '
|
||||
"See https://github.com/nonebot/nonebot2/pull/1453 .",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return Depends(_regex_matched, use_cache=False)
|
||||
|
||||
|
||||
def _regex_group(state: T_State):
|
||||
def _regex_str(state: T_State) -> str:
|
||||
return state[REGEX_STR]
|
||||
|
||||
|
||||
def RegexStr() -> str:
|
||||
"""正则匹配结果文本"""
|
||||
return Depends(_regex_str, use_cache=False)
|
||||
|
||||
|
||||
def _regex_group(state: T_State) -> Tuple[Any, ...]:
|
||||
return state[REGEX_GROUP]
|
||||
|
||||
|
||||
@@ -148,7 +175,7 @@ def RegexGroup() -> Tuple[Any, ...]:
|
||||
return Depends(_regex_group, use_cache=False)
|
||||
|
||||
|
||||
def _regex_dict(state: T_State):
|
||||
def _regex_dict(state: T_State) -> Dict[str, Any]:
|
||||
return state[REGEX_DICT]
|
||||
|
||||
|
||||
@@ -196,7 +223,7 @@ def Keyword() -> str:
|
||||
def Received(id: Optional[str] = None, default: Any = None) -> Any:
|
||||
"""`receive` 事件参数"""
|
||||
|
||||
def _received(matcher: "Matcher"):
|
||||
def _received(matcher: "Matcher") -> Any:
|
||||
return matcher.get_receive(id or "", default)
|
||||
|
||||
return Depends(_received, use_cache=False)
|
||||
|
@@ -65,6 +65,8 @@ def _revert_plugin(plugin: "Plugin") -> None:
|
||||
if plugin.name not in _plugins:
|
||||
raise RuntimeError("Plugin not found!")
|
||||
del _plugins[plugin.name]
|
||||
if parent_plugin := plugin.parent_plugin:
|
||||
parent_plugin.sub_plugins.remove(plugin)
|
||||
|
||||
|
||||
def get_plugin(name: str) -> Optional["Plugin"]:
|
||||
|
@@ -9,14 +9,17 @@ from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Set, Union, Iterable, Optional
|
||||
|
||||
import tomlkit
|
||||
|
||||
from nonebot.utils import path_to_module_name
|
||||
|
||||
from .plugin import Plugin
|
||||
from .manager import PluginManager
|
||||
from . import _managers, get_plugin, _module_name_to_plugin_name
|
||||
|
||||
try: # pragma: py-gte-311
|
||||
import tomllib # pyright: reportMissingImports=false
|
||||
except ModuleNotFoundError: # pragma: py-lt-311
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
||||
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
|
||||
@@ -108,7 +111,7 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
```
|
||||
"""
|
||||
with open(file_path, "r", encoding=encoding) as f:
|
||||
data = tomlkit.parse(f.read()) # type: ignore
|
||||
data = tomllib.loads(f.read())
|
||||
|
||||
nonebot_data = data.get("tool", {}).get("nonebot")
|
||||
if nonebot_data is None:
|
||||
|
@@ -14,7 +14,7 @@ from itertools import chain
|
||||
from types import ModuleType
|
||||
from importlib.abc import MetaPathFinder
|
||||
from importlib.machinery import PathFinder, SourceFileLoader
|
||||
from typing import Set, Dict, List, Union, Iterable, Optional, Sequence
|
||||
from typing import Set, Dict, List, Iterable, Optional, Sequence
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag, path_to_module_name
|
||||
@@ -148,14 +148,21 @@ class PluginManager:
|
||||
else:
|
||||
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
|
||||
|
||||
logger.opt(colors=True).success(
|
||||
f'Succeeded to import "<y>{escape_tag(name)}</y>"'
|
||||
)
|
||||
if (plugin := getattr(module, "__plugin__", None)) is None:
|
||||
if (
|
||||
plugin := getattr(module, "__plugin__", None)
|
||||
) is None or not isinstance(plugin, Plugin):
|
||||
raise RuntimeError(
|
||||
f"Module {module.__name__} is not loaded as a plugin! "
|
||||
"Make sure not to import it before loading."
|
||||
)
|
||||
logger.opt(colors=True).success(
|
||||
f'Succeeded to load plugin "<y>{escape_tag(plugin.name)}</y>"'
|
||||
+ (
|
||||
f' from "<m>{escape_tag(plugin.module_name)}</m>"'
|
||||
if plugin.module_name != plugin.name
|
||||
else ""
|
||||
)
|
||||
)
|
||||
return plugin
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
@@ -174,7 +181,7 @@ class PluginFinder(MetaPathFinder):
|
||||
def find_spec(
|
||||
self,
|
||||
fullname: str,
|
||||
path: Optional[Sequence[Union[bytes, str]]],
|
||||
path: Optional[Sequence[str]],
|
||||
target: Optional[ModuleType] = None,
|
||||
):
|
||||
if _managers:
|
||||
|
@@ -28,16 +28,44 @@ from nonebot.rule import (
|
||||
shell_command,
|
||||
)
|
||||
|
||||
from .plugin import Plugin
|
||||
from . import get_plugin_by_module_name
|
||||
from .manager import _current_plugin_chain
|
||||
|
||||
|
||||
def _store_matcher(matcher: Type[Matcher]) -> None:
|
||||
# only store the matcher defined in the plugin
|
||||
if plugins := _current_plugin_chain.get():
|
||||
plugins[-1].matcher.add(matcher)
|
||||
def store_matcher(matcher: Type[Matcher]) -> None:
|
||||
"""存储一个事件响应器到插件。
|
||||
|
||||
参数:
|
||||
matcher: 事件响应器
|
||||
"""
|
||||
# only store the matcher defined when plugin loading
|
||||
if plugin_chain := _current_plugin_chain.get():
|
||||
plugin_chain[-1].matcher.add(matcher)
|
||||
|
||||
|
||||
def _get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
|
||||
def get_matcher_plugin(depth: int = 1) -> Optional[Plugin]:
|
||||
"""获取事件响应器定义所在插件。
|
||||
|
||||
参数:
|
||||
depth: 调用栈深度
|
||||
"""
|
||||
# matcher defined when plugin loading
|
||||
if plugin_chain := _current_plugin_chain.get():
|
||||
return plugin_chain[-1]
|
||||
|
||||
# matcher defined when plugin running
|
||||
if module := get_matcher_module(depth + 1):
|
||||
if plugin := get_plugin_by_module_name(module.__name__):
|
||||
return plugin
|
||||
|
||||
|
||||
def get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
|
||||
"""获取事件响应器定义所在模块。
|
||||
|
||||
参数:
|
||||
depth: 调用栈深度
|
||||
"""
|
||||
current_frame = inspect.currentframe()
|
||||
if current_frame is None:
|
||||
return None
|
||||
@@ -71,7 +99,6 @@ def on(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
type,
|
||||
Rule() & rule,
|
||||
@@ -81,29 +108,20 @@ def on(
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
plugin=get_matcher_plugin(_depth + 1),
|
||||
module=get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
store_matcher(matcher)
|
||||
return matcher
|
||||
|
||||
|
||||
def on_metaevent(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = False,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_metaevent(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个元事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -111,36 +129,10 @@ def on_metaevent(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"meta_event",
|
||||
Rule() & rule,
|
||||
Permission(),
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
return on("meta_event", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_message(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = True,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_message(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个消息事件响应器。
|
||||
|
||||
参数:
|
||||
@@ -153,39 +145,16 @@ def on_message(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"message",
|
||||
Rule() & rule,
|
||||
Permission() | permission,
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
kwargs.setdefault("block", True)
|
||||
return on("message", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_notice(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = False,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_notice(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个通知事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -193,39 +162,15 @@ def on_notice(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"notice",
|
||||
Rule() & rule,
|
||||
Permission(),
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
return on("notice", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_request(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
block: bool = False,
|
||||
state: Optional[T_State] = None,
|
||||
_depth: int = 0,
|
||||
) -> Type[Matcher]:
|
||||
def on_request(*args, _depth: int = 0, **kwargs) -> Type[Matcher]:
|
||||
"""注册一个请求事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -233,22 +178,7 @@ def on_request(
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
"request",
|
||||
Rule() & rule,
|
||||
Permission(),
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
_store_matcher(matcher)
|
||||
return matcher
|
||||
return on("request", *args, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_startswith(
|
||||
@@ -349,6 +279,7 @@ def on_command(
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
@@ -360,6 +291,7 @@ def on_command(
|
||||
cmd: 指定命令内容
|
||||
rule: 事件响应规则
|
||||
aliases: 命令别名
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
@@ -370,9 +302,11 @@ def on_command(
|
||||
"""
|
||||
|
||||
commands = {cmd} | (aliases or set())
|
||||
block = kwargs.pop("block", False)
|
||||
kwargs.setdefault("block", False)
|
||||
return on_message(
|
||||
command(*commands) & rule, block=block, **kwargs, _depth=_depth + 1
|
||||
command(*commands, force_whitespace=force_whitespace) & rule,
|
||||
**kwargs,
|
||||
_depth=_depth + 1,
|
||||
)
|
||||
|
||||
|
||||
@@ -439,7 +373,7 @@ def on_regex(
|
||||
|
||||
|
||||
def on_type(
|
||||
types: Union[Type[Event], Tuple[Type[Event]]],
|
||||
types: Union[Type[Event], Tuple[Type[Event], ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
*,
|
||||
_depth: int = 0,
|
||||
@@ -518,6 +452,7 @@ class CommandGroup(_Group):
|
||||
参数:
|
||||
cmd: 指定命令内容
|
||||
aliases: 命令别名
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
@@ -587,6 +522,7 @@ class MatcherGroup(_Group):
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -622,6 +558,7 @@ class MatcherGroup(_Group):
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -639,6 +576,7 @@ class MatcherGroup(_Group):
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
@@ -736,6 +674,7 @@ class MatcherGroup(_Group):
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
||||
@@ -745,6 +684,7 @@ class MatcherGroup(_Group):
|
||||
参数:
|
||||
cmd: 指定命令内容
|
||||
aliases: 命令别名
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
@@ -755,7 +695,9 @@ class MatcherGroup(_Group):
|
||||
state: 默认 state
|
||||
"""
|
||||
final_kwargs = self._get_final_kwargs(kwargs, exclude={"type"})
|
||||
matcher = on_command(cmd, aliases=aliases, **final_kwargs)
|
||||
matcher = on_command(
|
||||
cmd, aliases=aliases, force_whitespace=force_whitespace, **final_kwargs
|
||||
)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from types import ModuleType
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Set, List, Type, Tuple, Union, Optional
|
||||
|
||||
@@ -9,6 +10,11 @@ from nonebot.dependencies import Dependent
|
||||
from nonebot.rule import Rule, ArgumentParser
|
||||
from nonebot.typing import T_State, T_Handler, T_RuleChecker, T_PermissionChecker
|
||||
|
||||
from .plugin import Plugin
|
||||
|
||||
def store_matcher(matcher: Type[Matcher]) -> None: ...
|
||||
def get_matcher_plugin(depth: int = ...) -> Optional[Plugin]: ...
|
||||
def get_matcher_module(depth: int = ...) -> Optional[ModuleType]: ...
|
||||
def on(
|
||||
type: str = "",
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
@@ -23,6 +29,7 @@ def on(
|
||||
) -> Type[Matcher]: ...
|
||||
def on_metaevent(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -44,6 +51,7 @@ def on_message(
|
||||
) -> Type[Matcher]: ...
|
||||
def on_notice(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -54,6 +62,7 @@ def on_notice(
|
||||
) -> Type[Matcher]: ...
|
||||
def on_request(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -117,6 +126,7 @@ def on_command(
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
@@ -154,7 +164,7 @@ def on_regex(
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_type(
|
||||
types: Union[Type[Event], Tuple[Type[Event]]],
|
||||
types: Union[Type[Event], Tuple[Type[Event], ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
@@ -186,6 +196,7 @@ class CommandGroup:
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
@@ -241,6 +252,7 @@ class MatcherGroup:
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
@@ -264,6 +276,7 @@ class MatcherGroup:
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
@@ -275,6 +288,7 @@ class MatcherGroup:
|
||||
self,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
@@ -341,6 +355,7 @@ class MatcherGroup:
|
||||
self,
|
||||
cmd: Union[str, Tuple[str, ...]],
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
|
||||
force_whitespace: Optional[Union[str, bool]] = ...,
|
||||
*,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
|
@@ -12,7 +12,8 @@ from pydantic import BaseModel
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
from . import _plugins as plugins # FIXME: backport for nonebug
|
||||
# FIXME: backport for nonebug
|
||||
from . import _plugins as plugins # nopycln: import
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import PluginManager
|
||||
@@ -46,7 +47,7 @@ class Plugin:
|
||||
manager: "PluginManager"
|
||||
"""导入该插件的插件管理器"""
|
||||
matcher: Set[Type[Matcher]] = field(default_factory=set)
|
||||
"""插件内定义的 `Matcher`"""
|
||||
"""插件加载时定义的 `Matcher`"""
|
||||
parent_plugin: Optional["Plugin"] = None
|
||||
"""父插件"""
|
||||
sub_plugins: Set["Plugin"] = field(default_factory=set)
|
||||
|
@@ -12,6 +12,7 @@ import re
|
||||
import shlex
|
||||
from argparse import Action
|
||||
from argparse import ArgumentError
|
||||
from contextvars import ContextVar
|
||||
from itertools import chain, product
|
||||
from argparse import Namespace as Namespace
|
||||
from argparse import ArgumentParser as ArgParser
|
||||
@@ -38,10 +39,11 @@ from nonebot.log import logger
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.exception import ParserExit
|
||||
from nonebot.internal.rule import Rule as Rule
|
||||
from nonebot.params import Command, EventToMe, CommandArg
|
||||
from nonebot.adapters import Bot, Event, Message, MessageSegment
|
||||
from nonebot.params import Command, EventToMe, CommandArg, CommandWhitespace
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
REGEX_STR,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
@@ -55,6 +57,7 @@ from nonebot.consts import (
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -66,6 +69,7 @@ CMD_RESULT = TypedDict(
|
||||
"raw_command": Optional[str],
|
||||
"command_arg": Optional[Message[MessageSegment]],
|
||||
"command_start": Optional[str],
|
||||
"command_whitespace": Optional[str],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -73,6 +77,8 @@ TRIE_VALUE = NamedTuple(
|
||||
"TRIE_VALUE", [("command_start", str), ("command", Tuple[str, ...])]
|
||||
)
|
||||
|
||||
parser_message: ContextVar[str] = ContextVar("parser_message")
|
||||
|
||||
|
||||
class TrieRule:
|
||||
prefix: CharTrie = CharTrie()
|
||||
@@ -87,7 +93,11 @@ class TrieRule:
|
||||
@classmethod
|
||||
def get_value(cls, bot: Bot, event: Event, state: T_State) -> CMD_RESULT:
|
||||
prefix = CMD_RESULT(
|
||||
command=None, raw_command=None, command_arg=None, command_start=None
|
||||
command=None,
|
||||
raw_command=None,
|
||||
command_arg=None,
|
||||
command_start=None,
|
||||
command_whitespace=None,
|
||||
)
|
||||
state[PREFIX_KEY] = prefix
|
||||
if event.get_type() != "message":
|
||||
@@ -102,11 +112,25 @@ class TrieRule:
|
||||
prefix[RAW_CMD_KEY] = pf.key
|
||||
prefix[CMD_START_KEY] = value.command_start
|
||||
prefix[CMD_KEY] = value.command
|
||||
|
||||
msg = message.copy()
|
||||
msg.pop(0)
|
||||
new_message = msg.__class__(segment_text[len(pf.key) :].lstrip())
|
||||
for new_segment in reversed(new_message):
|
||||
msg.insert(0, new_segment)
|
||||
|
||||
# check whitespace
|
||||
arg_str = segment_text[len(pf.key) :]
|
||||
arg_str_stripped = arg_str.lstrip()
|
||||
has_arg = arg_str_stripped or msg
|
||||
if (
|
||||
has_arg
|
||||
and (stripped_len := len(arg_str) - len(arg_str_stripped)) > 0
|
||||
):
|
||||
prefix[CMD_WHITESPACE_KEY] = arg_str[:stripped_len]
|
||||
|
||||
# construct command arg
|
||||
if arg_str_stripped:
|
||||
new_message = msg.__class__(arg_str_stripped)
|
||||
for new_segment in reversed(new_message):
|
||||
msg.insert(0, new_segment)
|
||||
prefix[CMD_ARG_KEY] = msg
|
||||
|
||||
return prefix
|
||||
@@ -140,8 +164,6 @@ class StartswithRule:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
@@ -197,8 +219,6 @@ class EndswithRule:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
@@ -254,8 +274,6 @@ class FullmatchRule:
|
||||
return hash((frozenset(self.msg), self.ignorecase))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
@@ -306,8 +324,6 @@ class KeywordsRule:
|
||||
return hash(frozenset(self.keywords))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
try:
|
||||
text = event.get_plaintext()
|
||||
except Exception:
|
||||
@@ -335,12 +351,18 @@ class CommandRule:
|
||||
|
||||
参数:
|
||||
cmds: 指定命令元组列表
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
"""
|
||||
|
||||
__slots__ = ("cmds",)
|
||||
__slots__ = ("cmds", "force_whitespace")
|
||||
|
||||
def __init__(self, cmds: List[Tuple[str, ...]]):
|
||||
def __init__(
|
||||
self,
|
||||
cmds: List[Tuple[str, ...]],
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
):
|
||||
self.cmds = tuple(cmds)
|
||||
self.force_whitespace = force_whitespace
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Command(cmds={self.cmds})"
|
||||
@@ -353,11 +375,24 @@ class CommandRule:
|
||||
def __hash__(self) -> int:
|
||||
return hash((frozenset(self.cmds),))
|
||||
|
||||
async def __call__(self, cmd: Optional[Tuple[str, ...]] = Command()) -> bool:
|
||||
return cmd in self.cmds
|
||||
async def __call__(
|
||||
self,
|
||||
cmd: Optional[Tuple[str, ...]] = Command(),
|
||||
cmd_whitespace: Optional[str] = CommandWhitespace(),
|
||||
) -> bool:
|
||||
if cmd not in self.cmds:
|
||||
return False
|
||||
if self.force_whitespace is None:
|
||||
return True
|
||||
if isinstance(self.force_whitespace, str):
|
||||
return self.force_whitespace == cmd_whitespace
|
||||
return self.force_whitespace == (cmd_whitespace is not None)
|
||||
|
||||
|
||||
def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
def command(
|
||||
*cmds: Union[str, Tuple[str, ...]],
|
||||
force_whitespace: Optional[Union[str, bool]] = None,
|
||||
) -> Rule:
|
||||
"""匹配消息命令。
|
||||
|
||||
根据配置里提供的 {ref}``command_start` <nonebot.config.Config.command_start>`,
|
||||
@@ -369,6 +404,7 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
|
||||
参数:
|
||||
cmds: 命令文本或命令元组
|
||||
force_whitespace: 是否强制命令后必须有指定空白符
|
||||
|
||||
用法:
|
||||
使用默认 `command_start`, `command_sep` 配置
|
||||
@@ -400,7 +436,7 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
f"{start}{sep.join(command)}", TRIE_VALUE(start, command)
|
||||
)
|
||||
|
||||
return Rule(CommandRule(commands))
|
||||
return Rule(CommandRule(commands, force_whitespace))
|
||||
|
||||
|
||||
class ArgumentParser(ArgParser):
|
||||
@@ -446,13 +482,15 @@ class ArgumentParser(ArgParser):
|
||||
)
|
||||
|
||||
def _print_message(self, message: str, file: Optional[IO[str]] = None):
|
||||
if message:
|
||||
setattr(self, "_message", getattr(self, "_message", "") + message)
|
||||
if (msg := parser_message.get(None)) is not None:
|
||||
parser_message.set(msg + message)
|
||||
else:
|
||||
super()._print_message(message, file)
|
||||
|
||||
def exit(self, status: int = 0, message: Optional[str] = None):
|
||||
if message:
|
||||
self._print_message(message)
|
||||
raise ParserExit(status=status, message=getattr(self, "_message", None))
|
||||
raise ParserExit(status=status, message=parser_message.get(None))
|
||||
|
||||
|
||||
class ShellCommandRule:
|
||||
@@ -499,13 +537,16 @@ class ShellCommandRule:
|
||||
)
|
||||
|
||||
if self.parser:
|
||||
t = parser_message.set("")
|
||||
try:
|
||||
args = self.parser.parse_args(state[SHELL_ARGV])
|
||||
state[SHELL_ARGS] = args
|
||||
except ArgumentError as e:
|
||||
except ArgumentError as e: # pragma: py-gte-39
|
||||
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
|
||||
except ParserExit as e:
|
||||
state[SHELL_ARGS] = e
|
||||
finally:
|
||||
parser_message.reset(t)
|
||||
return True
|
||||
|
||||
|
||||
@@ -600,14 +641,13 @@ class RegexRule:
|
||||
return hash((self.regex, self.flags))
|
||||
|
||||
async def __call__(self, event: Event, state: T_State) -> bool:
|
||||
if event.get_type() != "message":
|
||||
return False
|
||||
try:
|
||||
msg = event.get_message()
|
||||
except Exception:
|
||||
return False
|
||||
if matched := re.search(self.regex, str(msg), self.flags):
|
||||
state[REGEX_MATCHED] = matched.group()
|
||||
state[REGEX_STR] = matched.group()
|
||||
state[REGEX_GROUP] = matched.groups()
|
||||
state[REGEX_DICT] = matched.groupdict()
|
||||
return True
|
||||
@@ -618,7 +658,7 @@ class RegexRule:
|
||||
def regex(regex: str, flags: Union[int, re.RegexFlag] = 0) -> Rule:
|
||||
"""匹配符合正则表达式的消息字符串。
|
||||
|
||||
可以通过 {ref}`nonebot.params.RegexMatched` 获取匹配成功的字符串,
|
||||
可以通过 {ref}`nonebot.params.RegexStr` 获取匹配成功的字符串,
|
||||
通过 {ref}`nonebot.params.RegexGroup` 获取匹配成功的 group 元组,
|
||||
通过 {ref}`nonebot.params.RegexDict` 获取匹配成功的 group 字典。
|
||||
|
||||
|
@@ -9,6 +9,7 @@ import re
|
||||
import json
|
||||
import asyncio
|
||||
import inspect
|
||||
import importlib
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
from functools import wraps, partial
|
||||
@@ -167,13 +168,30 @@ def get_name(obj: Any) -> str:
|
||||
|
||||
|
||||
def path_to_module_name(path: Path) -> str:
|
||||
rel_path = path.resolve().relative_to(Path(".").resolve())
|
||||
"""转换路径为模块名"""
|
||||
rel_path = path.resolve().relative_to(Path.cwd().resolve())
|
||||
if rel_path.stem == "__init__":
|
||||
return ".".join(rel_path.parts[:-1])
|
||||
else:
|
||||
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
||||
|
||||
|
||||
def resolve_dot_notation(
|
||||
obj_str: str, default_attr: str, default_prefix: Optional[str] = None
|
||||
) -> Any:
|
||||
"""解析并导入点分表示法的对象"""
|
||||
modulename, _, cls = obj_str.partition(":")
|
||||
if default_prefix is not None and modulename.startswith("~"):
|
||||
modulename = default_prefix + modulename[1:]
|
||||
module = importlib.import_module(modulename)
|
||||
if not cls:
|
||||
return getattr(module, default_attr)
|
||||
instance = module
|
||||
for attr_str in cls.split("."):
|
||||
instance = getattr(instance, attr_str)
|
||||
return instance
|
||||
|
||||
|
||||
class DataclassEncoder(json.JSONEncoder):
|
||||
"""在JSON序列化 {re}`nonebot.adapters._message.Message` (List[Dataclass]) 时使用的 `JSONEncoder`"""
|
||||
|
||||
|
@@ -11,7 +11,7 @@
|
||||
"start": "yarn workspace nonebot start",
|
||||
"serve": "yarn workspace nonebot serve",
|
||||
"clear": "yarn workspace nonebot clear",
|
||||
"prettier": "prettier --config ./.prettierrc --write \"./website/**/*.md\""
|
||||
"prettier": "prettier --config ./.prettierrc --write \"./website/\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
|
3385
poetry.lock
generated
3385
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "nonebot2"
|
||||
version = "2.0.0rc2"
|
||||
version = "2.0.0rc4"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
@@ -26,42 +26,45 @@ python = "^3.8"
|
||||
yarl = "^1.7.2"
|
||||
loguru = "^0.6.0"
|
||||
pygtrie = "^2.4.1"
|
||||
fastapi = ">=0.87.0,<1.0.0"
|
||||
tomlkit = ">=0.10.0,<1.0.0"
|
||||
typing-extensions = ">=3.10.0,<5.0.0"
|
||||
typing-extensions = ">=4.0.0,<5.0.0"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
pydantic = { version = "^1.10.0", extras = ["dotenv"] }
|
||||
uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"] }
|
||||
|
||||
websockets = { version="^10.0", optional = true }
|
||||
websockets = { version = "^10.0", optional = true }
|
||||
Quart = { version = ">=0.18.0,<1.0.0", optional = true }
|
||||
fastapi = { version = ">=0.93.0,<1.0.0", optional = true }
|
||||
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
|
||||
httpx = { version = ">=0.20.0,<1.0.0", extras = ["http2"], optional = true }
|
||||
uvicorn = { version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pycln = "^2.1.2"
|
||||
isort = "^5.10.1"
|
||||
black = "^22.1.0"
|
||||
black = "^23.1.0"
|
||||
nonemoji = "^0.1.2"
|
||||
pre-commit = "^2.16.0"
|
||||
pre-commit = "^3.0.0"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
nonebug = "^0.3.0"
|
||||
pytest-cov = "^4.0.0"
|
||||
pytest-xdist = "^3.0.2"
|
||||
pytest-asyncio = "^0.20.0"
|
||||
nonebug = { git = "https://github.com/nonebot/nonebug.git" }
|
||||
pytest-asyncio = "^0.21.0"
|
||||
coverage-conditional-plugin = "^0.8.0"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
nb-autodoc = { git = "https://github.com/nonebot/nb-autodoc.git" }
|
||||
nb-autodoc = "^1.0.0a5"
|
||||
|
||||
[tool.poetry.extras]
|
||||
quart = ["quart"]
|
||||
httpx = ["httpx"]
|
||||
aiohttp = ["aiohttp"]
|
||||
websockets = ["websockets"]
|
||||
all = ["quart", "aiohttp", "httpx", "websockets"]
|
||||
quart = ["quart", "uvicorn"]
|
||||
fastapi = ["fastapi", "uvicorn"]
|
||||
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
addopts = "--cov=nonebot --cov-report=term-missing"
|
||||
addopts = "--cov=nonebot --cov-append --cov-report=term-missing"
|
||||
filterwarnings = [
|
||||
"error",
|
||||
"ignore::DeprecationWarning",
|
||||
@@ -83,6 +86,20 @@ force_sort_within_sections = true
|
||||
src_paths = ["nonebot", "tests"]
|
||||
extra_standard_library = ["typing_extensions"]
|
||||
|
||||
[tool.pycln]
|
||||
path = "."
|
||||
all = false
|
||||
|
||||
[tool.pyright]
|
||||
reportShadowedImports = false
|
||||
pythonVersion = "3.8"
|
||||
pythonPlatform = "All"
|
||||
executionEnvironments = [
|
||||
{ root = "./tests", extraPaths = ["./"] },
|
||||
{ root = "./" },
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry_core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
@@ -1,9 +1,25 @@
|
||||
[run]
|
||||
plugins =
|
||||
coverage_conditional_plugin
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
def __repr__
|
||||
def __str__
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
@(typing\.)?overload
|
||||
if (typing\.)?TYPE_CHECKING( is True)?:
|
||||
@(abc\.)?abstractmethod
|
||||
raise NotImplementedError
|
||||
\.\.\.
|
||||
pass
|
||||
if __name__ == .__main__.:
|
||||
|
||||
[coverage_conditional_plugin]
|
||||
rules =
|
||||
"sys_platform != 'win32'": py-win32
|
||||
"sys_platform != 'linux'": py-linux
|
||||
"sys_platform != 'darwin'": py-darwin
|
||||
"sys_version_info < (3, 9)": py-gte-39
|
||||
"sys_version_info < (3, 11)": py-gte-311
|
||||
"sys_version_info >= (3, 11)": py-lt-311
|
||||
|
@@ -1,21 +1,30 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
import pytest
|
||||
from nonebug import NONEBOT_INIT_KWARGS
|
||||
|
||||
import nonebot
|
||||
|
||||
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
||||
os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||
import nonebot
|
||||
def pytest_configure(config: pytest.Config) -> None:
|
||||
config.stash[NONEBOT_INIT_KWARGS] = {"config_from_init": "init"}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def load_plugin(nonebug_init: None) -> Set["Plugin"]:
|
||||
# preload global plugins
|
||||
return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def load_example(nonebug_init: None) -> Set["Plugin"]:
|
||||
import nonebot
|
||||
|
||||
# preload example plugins
|
||||
return nonebot.load_plugins(str(Path(__file__).parent / "examples"))
|
||||
|
0
tests/dynamic/path.py
Normal file
0
tests/dynamic/path.py
Normal file
0
tests/dynamic/require_not_declared.py
Normal file
0
tests/dynamic/require_not_declared.py
Normal file
0
tests/dynamic/require_not_loaded.py
Normal file
0
tests/dynamic/require_not_loaded.py
Normal file
0
tests/dynamic/simple.py
Normal file
0
tests/dynamic/simple.py
Normal file
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"plugins": [],
|
||||
"plugin_dirs": ["plugins"]
|
||||
"plugin_dirs": []
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
[tool.nonebot]
|
||||
plugins = []
|
||||
plugin_dirs = ["plugins"]
|
||||
plugin_dirs = []
|
||||
|
@@ -90,3 +90,6 @@ async def overload(event: FakeEvent):
|
||||
@test_overload.handle()
|
||||
async def finish():
|
||||
await test_overload.finish()
|
||||
|
||||
|
||||
test_destroy = on_message()
|
||||
|
@@ -1 +1 @@
|
||||
from .nested_subplugin2 import a
|
||||
from .nested_subplugin2 import a # nopycln: import
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from nonebot import on_message
|
||||
from nonebot.params import Depends
|
||||
@@ -47,3 +48,17 @@ async def depends_cache(y: int = Depends(dependency, use_cache=True)):
|
||||
|
||||
async def class_depend(c: ClassDependency = Depends()):
|
||||
return c
|
||||
|
||||
|
||||
async def annotated_depend(x: Annotated[int, Depends(dependency)]):
|
||||
return x
|
||||
|
||||
|
||||
async def annotated_class_depend(c: Annotated[ClassDependency, Depends()]):
|
||||
return c
|
||||
|
||||
|
||||
async def annotated_prior_depend(
|
||||
x: Annotated[int, Depends(lambda: 2)] = Depends(dependency)
|
||||
):
|
||||
return x
|
||||
|
@@ -6,6 +6,7 @@ from nonebot.params import (
|
||||
Command,
|
||||
Keyword,
|
||||
Endswith,
|
||||
RegexStr,
|
||||
Fullmatch,
|
||||
RegexDict,
|
||||
CommandArg,
|
||||
@@ -16,6 +17,7 @@ from nonebot.params import (
|
||||
RegexMatched,
|
||||
ShellCommandArgs,
|
||||
ShellCommandArgv,
|
||||
CommandWhitespace,
|
||||
)
|
||||
|
||||
|
||||
@@ -47,6 +49,10 @@ async def command_start(start: str = CommandStart()) -> str:
|
||||
return start
|
||||
|
||||
|
||||
async def command_whitespace(whitespace: str = CommandWhitespace()) -> str:
|
||||
return whitespace
|
||||
|
||||
|
||||
async def shell_command_args(
|
||||
shell_command_args: dict = ShellCommandArgs(),
|
||||
) -> dict:
|
||||
@@ -71,6 +77,10 @@ async def regex_matched(regex_matched: str = RegexMatched()) -> str:
|
||||
return regex_matched
|
||||
|
||||
|
||||
async def regex_str(regex_matched: str = RegexStr()) -> str:
|
||||
return regex_matched
|
||||
|
||||
|
||||
async def startswith(startswith: str = Startswith()) -> str:
|
||||
return startswith
|
||||
|
||||
|
@@ -1,6 +1,8 @@
|
||||
from typing import Type
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot import (
|
||||
CommandGroup,
|
||||
MatcherGroup,
|
||||
@@ -50,6 +52,20 @@ matcher_on = on(
|
||||
)
|
||||
|
||||
|
||||
def matcher_on_factory() -> Type[Matcher]:
|
||||
return on(
|
||||
"test",
|
||||
rule=rule,
|
||||
permission=permission,
|
||||
handlers=[handler],
|
||||
temp=True,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=True,
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
matcher_on_metaevent = on_metaevent(
|
||||
rule=rule,
|
||||
handlers=[handler],
|
||||
|
@@ -87,7 +87,6 @@ def test_message_add():
|
||||
|
||||
|
||||
def test_message_getitem():
|
||||
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
@@ -102,7 +101,7 @@ def test_message_getitem():
|
||||
|
||||
assert message[0] == MessageSegment.text("test")
|
||||
|
||||
assert message[0:2] == Message(
|
||||
assert message[:2] == Message(
|
||||
[MessageSegment.text("test"), MessageSegment.image("test2")]
|
||||
)
|
||||
|
||||
|
@@ -1,9 +1,8 @@
|
||||
from nonebot.adapters import MessageTemplate
|
||||
from utils import escape_text, make_fake_message
|
||||
|
||||
|
||||
def test_template_basis():
|
||||
from nonebot.adapters import MessageTemplate
|
||||
|
||||
template = MessageTemplate("{key:.3%}")
|
||||
formatted = template.format(key=0.123456789)
|
||||
assert formatted == "12.346%"
|
||||
|
@@ -1,32 +1,86 @@
|
||||
from typing import cast
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Any, Set, cast
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
import nonebot
|
||||
from nonebot.config import Env
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.params import Depends
|
||||
from nonebot import _resolve_combine_expr
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers._lifespan import Lifespan
|
||||
from nonebot.drivers import (
|
||||
URL,
|
||||
Driver,
|
||||
Request,
|
||||
Response,
|
||||
WebSocket,
|
||||
ForwardDriver,
|
||||
ReverseDriver,
|
||||
HTTPServerSetup,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="driver")
|
||||
def load_driver(request: pytest.FixtureRequest) -> Driver:
|
||||
driver_name = getattr(request, "param", None)
|
||||
global_driver = nonebot.get_driver()
|
||||
if driver_name is None:
|
||||
return global_driver
|
||||
|
||||
DriverClass = _resolve_combine_expr(driver_name)
|
||||
return DriverClass(Env(environment=global_driver.env), global_driver.config)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifespan():
|
||||
lifespan = Lifespan()
|
||||
|
||||
start_log = []
|
||||
shutdown_log = []
|
||||
|
||||
@lifespan.on_startup
|
||||
async def _startup1():
|
||||
assert start_log == []
|
||||
start_log.append(1)
|
||||
|
||||
@lifespan.on_startup
|
||||
async def _startup2():
|
||||
assert start_log == [1]
|
||||
start_log.append(2)
|
||||
|
||||
@lifespan.on_shutdown
|
||||
async def _shutdown1():
|
||||
assert shutdown_log == []
|
||||
shutdown_log.append(1)
|
||||
|
||||
@lifespan.on_shutdown
|
||||
async def _shutdown2():
|
||||
assert shutdown_log == [1]
|
||||
shutdown_log.append(2)
|
||||
|
||||
async with lifespan:
|
||||
assert start_log == [1, 2]
|
||||
|
||||
assert shutdown_log == [1, 2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"nonebug_init",
|
||||
"driver",
|
||||
[
|
||||
pytest.param({"driver": "nonebot.drivers.fastapi:Driver"}, id="fastapi"),
|
||||
pytest.param({"driver": "nonebot.drivers.quart:Driver"}, id="quart"),
|
||||
pytest.param("nonebot.drivers.fastapi:Driver", id="fastapi"),
|
||||
pytest.param("nonebot.drivers.quart:Driver", id="quart"),
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_reverse_driver(app: App):
|
||||
import nonebot
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers import (
|
||||
URL,
|
||||
Request,
|
||||
Response,
|
||||
WebSocket,
|
||||
ReverseDriver,
|
||||
HTTPServerSetup,
|
||||
WebSocketServerSetup,
|
||||
)
|
||||
|
||||
driver = cast(ReverseDriver, nonebot.get_driver())
|
||||
async def test_reverse_driver(app: App, driver: Driver):
|
||||
driver = cast(ReverseDriver, driver)
|
||||
|
||||
async def _handle_http(request: Request) -> Response:
|
||||
assert request.content in (b"test", "test")
|
||||
@@ -59,7 +113,7 @@ async def test_reverse_driver(app: App):
|
||||
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
|
||||
driver.setup_websocket_server(ws_setup)
|
||||
|
||||
async with app.test_server() as ctx:
|
||||
async with app.test_server(driver.asgi) as ctx:
|
||||
client = ctx.get_client()
|
||||
response = await client.post("/http_test", data="test")
|
||||
assert response.status_code == 200
|
||||
@@ -79,26 +133,142 @@ async def test_reverse_driver(app: App):
|
||||
|
||||
await ws.close()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"nonebug_init, driver_type",
|
||||
"driver",
|
||||
[
|
||||
pytest.param("nonebot.drivers.httpx:Driver", id="httpx"),
|
||||
pytest.param("nonebot.drivers.aiohttp:Driver", id="aiohttp"),
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_http_driver(driver: Driver):
|
||||
driver = cast(ForwardDriver, driver)
|
||||
|
||||
request = Request(
|
||||
"POST",
|
||||
"https://httpbin.org/post",
|
||||
params={"param": "test"},
|
||||
headers={"X-Test": "test"},
|
||||
cookies={"session": "test"},
|
||||
content="test",
|
||||
)
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["args"] == {"param": "test"}
|
||||
assert data["headers"].get("X-Test") == "test"
|
||||
assert data["headers"].get("Cookie") == "session=test"
|
||||
assert data["data"] == "test"
|
||||
|
||||
request = Request("POST", "https://httpbin.org/post", data={"form": "test"})
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["form"] == {"form": "test"}
|
||||
|
||||
request = Request("POST", "https://httpbin.org/post", json={"json": "test"})
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["json"] == {"json": "test"}
|
||||
|
||||
request = Request(
|
||||
"POST", "https://httpbin.org/post", files={"test": ("test.txt", b"test")}
|
||||
)
|
||||
response = await driver.request(request)
|
||||
assert response.status_code == 200 and response.content
|
||||
data = json.loads(response.content)
|
||||
assert data["files"] == {"test": "test"}
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver, driver_type",
|
||||
[
|
||||
pytest.param(
|
||||
{"driver": "nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin"},
|
||||
"nonebot.drivers.fastapi:Driver+nonebot.drivers.aiohttp:Mixin",
|
||||
"fastapi+aiohttp",
|
||||
id="fastapi+aiohttp",
|
||||
),
|
||||
pytest.param(
|
||||
{"driver": "~httpx:Driver+~websockets"},
|
||||
"block_driver+httpx+websockets",
|
||||
"~httpx:Driver+~websockets",
|
||||
"none+httpx+websockets",
|
||||
id="httpx+websockets",
|
||||
),
|
||||
],
|
||||
indirect=["nonebug_init"],
|
||||
indirect=["driver"],
|
||||
)
|
||||
async def test_combine_driver(app: App, driver_type: str):
|
||||
import nonebot
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
async def test_combine_driver(driver: Driver, driver_type: str):
|
||||
assert driver.type == driver_type
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot_connect_hook(app: App, driver: Driver):
|
||||
with pytest.MonkeyPatch.context() as m:
|
||||
conn_hooks: Set[Dependent[Any]] = set()
|
||||
disconn_hooks: Set[Dependent[Any]] = set()
|
||||
m.setattr(Driver, "_bot_connection_hook", conn_hooks)
|
||||
m.setattr(Driver, "_bot_disconnection_hook", disconn_hooks)
|
||||
|
||||
conn_should_be_called = False
|
||||
disconn_should_be_called = False
|
||||
dependency_should_be_run = False
|
||||
dependency_should_be_cleaned = False
|
||||
|
||||
async def dependency():
|
||||
nonlocal dependency_should_be_run, dependency_should_be_cleaned
|
||||
dependency_should_be_run = True
|
||||
try:
|
||||
yield 1
|
||||
finally:
|
||||
dependency_should_be_cleaned = True
|
||||
|
||||
@driver.on_bot_connect
|
||||
async def conn_hook(foo: Bot, dep: int = Depends(dependency), default: int = 1):
|
||||
nonlocal conn_should_be_called
|
||||
conn_should_be_called = True
|
||||
|
||||
if foo is not bot:
|
||||
pytest.fail("on_bot_connect hook called with wrong bot")
|
||||
if dep != 1:
|
||||
pytest.fail("on_bot_connect hook called with wrong dependency")
|
||||
if default != 1:
|
||||
pytest.fail("on_bot_connect hook called with wrong default value")
|
||||
|
||||
@driver.on_bot_disconnect
|
||||
async def disconn_hook(
|
||||
foo: Bot, dep: int = Depends(dependency), default: int = 1
|
||||
):
|
||||
nonlocal disconn_should_be_called
|
||||
disconn_should_be_called = True
|
||||
|
||||
if foo is not bot:
|
||||
pytest.fail("on_bot_disconnect hook called with wrong bot")
|
||||
if dep != 1:
|
||||
pytest.fail("on_bot_connect hook called with wrong dependency")
|
||||
if default != 1:
|
||||
pytest.fail("on_bot_connect hook called with wrong default value")
|
||||
|
||||
if conn_hook not in {hook.call for hook in conn_hooks}:
|
||||
pytest.fail("on_bot_connect hook not registered")
|
||||
if disconn_hook not in {hook.call for hook in disconn_hooks}:
|
||||
pytest.fail("on_bot_disconnect hook not registered")
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
if not conn_should_be_called:
|
||||
pytest.fail("on_bot_connect hook not called")
|
||||
if not disconn_should_be_called:
|
||||
pytest.fail("on_bot_disconnect hook not called")
|
||||
if not dependency_should_be_run:
|
||||
pytest.fail("dependency not run")
|
||||
if not dependency_should_be_cleaned:
|
||||
pytest.fail("dependency not cleaned")
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from utils import make_fake_event, make_fake_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weather(app: App, load_example):
|
||||
async def test_weather(app: App):
|
||||
from examples.weather import weather
|
||||
from utils import make_fake_event, make_fake_message
|
||||
|
||||
# 将此处的 make_fake_message() 替换为你要发送的平台消息 Message 类型
|
||||
# from nonebot.adapters.console import Message
|
||||
Message = make_fake_message()
|
||||
|
||||
async with app.test_matcher(weather) as ctx:
|
||||
@@ -15,6 +17,8 @@ async def test_weather(app: App, load_example):
|
||||
|
||||
msg = Message("/天气 上海")
|
||||
# 将此处的 make_fake_event() 替换为你要发送的平台事件 Event 类型
|
||||
# from nonebot.adapters.console import MessageEvent
|
||||
# event = MessageEvent(message=msg, to_me=True, ...)
|
||||
event = make_fake_event(_message=msg, _to_me=True)()
|
||||
|
||||
ctx.receive_event(bot, event)
|
||||
|
@@ -1,30 +1,25 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
||||
os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
import nonebot
|
||||
from nonebot.drivers import Driver, ReverseDriver
|
||||
from nonebot import (
|
||||
get_app,
|
||||
get_bot,
|
||||
get_asgi,
|
||||
get_bots,
|
||||
get_driver,
|
||||
get_adapter,
|
||||
get_adapters,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"nonebug_init",
|
||||
[
|
||||
{
|
||||
"config_from_init": "init",
|
||||
"driver": "~fastapi+~httpx+~websockets",
|
||||
},
|
||||
{"config_from_init": "init", "driver": "~fastapi+~aiohttp"},
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_init(nonebug_init):
|
||||
from nonebot import get_driver
|
||||
|
||||
env = get_driver().env
|
||||
async def test_init():
|
||||
env = nonebot.get_driver().env
|
||||
assert env == "test"
|
||||
|
||||
config = get_driver().config
|
||||
config = nonebot.get_driver().config
|
||||
assert config.config_from_env == {"test": "test"}
|
||||
assert config.config_override == "new"
|
||||
assert config.config_from_init == "init"
|
||||
@@ -36,21 +31,29 @@ async def test_init(nonebug_init):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear):
|
||||
import nonebot
|
||||
from nonebot.drivers import ForwardDriver, ReverseDriver
|
||||
from nonebot import get_app, get_bot, get_asgi, get_bots, get_driver
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
get_driver()
|
||||
|
||||
nonebot.init(driver="nonebot.drivers.fastapi")
|
||||
async def test_get(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(nonebot, "_driver", None)
|
||||
with pytest.raises(ValueError):
|
||||
get_driver()
|
||||
|
||||
driver = get_driver()
|
||||
assert isinstance(driver, ReverseDriver)
|
||||
assert get_asgi() == driver.asgi
|
||||
assert get_app() == driver.server_app
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
adapter = ctx.create_adapter()
|
||||
adapter_name = adapter.get_name()
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(Driver, "_adapters", {adapter_name: adapter})
|
||||
assert get_adapters() == {adapter_name: adapter}
|
||||
assert get_adapter(adapter_name) is adapter
|
||||
assert get_adapter(adapter.__class__) is adapter
|
||||
with pytest.raises(ValueError):
|
||||
get_adapter("not exist")
|
||||
|
||||
runned = False
|
||||
|
||||
def mock_run(*args, **kwargs):
|
||||
|
@@ -1,11 +1,14 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.permission import User
|
||||
from nonebot.message import _check_matcher
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from utils import make_fake_event, make_fake_message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App, load_plugin):
|
||||
async def test_matcher(app: App):
|
||||
from plugins.matcher.matcher_process import (
|
||||
test_got,
|
||||
test_handle,
|
||||
@@ -77,7 +80,21 @@ async def test_matcher(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_type_updater(app: App, load_plugin):
|
||||
async def test_matcher_destroy(app: App):
|
||||
from plugins.matcher.matcher_process import test_destroy
|
||||
|
||||
async with app.test_matcher(test_destroy) as ctx:
|
||||
assert len(matchers) == 1
|
||||
assert len(matchers[test_destroy.priority]) == 1
|
||||
assert matchers[test_destroy.priority][0] is test_destroy
|
||||
|
||||
test_destroy.destroy()
|
||||
|
||||
assert len(matchers[test_destroy.priority]) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_type_updater(app: App):
|
||||
from plugins.matcher.matcher_type import test_type_updater, test_custom_updater
|
||||
|
||||
event = make_fake_event()()
|
||||
@@ -98,8 +115,7 @@ async def test_type_updater(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission_updater(app: App, load_plugin):
|
||||
from nonebot.permission import User
|
||||
async def test_permission_updater(app: App):
|
||||
from plugins.matcher.matcher_permission import (
|
||||
default_permission,
|
||||
test_custom_updater,
|
||||
@@ -143,40 +159,37 @@ async def test_permission_updater(app: App, load_plugin):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run(app: App):
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
with app.provider.context({}):
|
||||
assert not matchers
|
||||
event = make_fake_event()()
|
||||
|
||||
assert not matchers
|
||||
event = make_fake_event()()
|
||||
async def reject():
|
||||
await Matcher.reject()
|
||||
|
||||
async def reject():
|
||||
await Matcher.reject()
|
||||
test_reject = Matcher.new(handlers=[reject])
|
||||
|
||||
test_reject = Matcher.new(handlers=[reject])
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
await test_reject().run(bot, event, {})
|
||||
assert len(matchers[0]) == 1
|
||||
assert len(matchers[0][0].handlers) == 1
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
await test_reject().run(bot, event, {})
|
||||
assert len(matchers[0]) == 1
|
||||
assert len(matchers[0][0].handlers) == 1
|
||||
del matchers[0]
|
||||
|
||||
del matchers[0]
|
||||
async def pause():
|
||||
await Matcher.pause()
|
||||
|
||||
async def pause():
|
||||
await Matcher.pause()
|
||||
test_pause = Matcher.new(handlers=[pause])
|
||||
|
||||
test_pause = Matcher.new(handlers=[pause])
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
await test_pause().run(bot, event, {})
|
||||
assert len(matchers[0]) == 1
|
||||
assert len(matchers[0][0].handlers) == 0
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
await test_pause().run(bot, event, {})
|
||||
assert len(matchers[0]) == 1
|
||||
assert len(matchers[0][0].handlers) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expire(app: App, load_plugin):
|
||||
from nonebot.matcher import matchers
|
||||
from nonebot.message import _check_matcher
|
||||
async def test_expire(app: App):
|
||||
from plugins.matcher.matcher_expire import (
|
||||
test_temp_matcher,
|
||||
test_datetime_matcher,
|
||||
@@ -187,25 +200,19 @@ async def test_expire(app: App, load_plugin):
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert test_temp_matcher in matchers[test_temp_matcher.priority]
|
||||
await _check_matcher(
|
||||
test_temp_matcher.priority, test_temp_matcher, bot, event, {}
|
||||
)
|
||||
await _check_matcher(test_temp_matcher, bot, event, {})
|
||||
assert test_temp_matcher not in matchers[test_temp_matcher.priority]
|
||||
|
||||
event = make_fake_event()()
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert test_datetime_matcher in matchers[test_datetime_matcher.priority]
|
||||
await _check_matcher(
|
||||
test_datetime_matcher.priority, test_datetime_matcher, bot, event, {}
|
||||
)
|
||||
await _check_matcher(test_datetime_matcher, bot, event, {})
|
||||
assert test_datetime_matcher not in matchers[test_datetime_matcher.priority]
|
||||
|
||||
event = make_fake_event()()
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority]
|
||||
await _check_matcher(
|
||||
test_timedelta_matcher.priority, test_timedelta_matcher, bot, event, {}
|
||||
)
|
||||
await _check_matcher(test_timedelta_matcher, bot, event, {})
|
||||
assert test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]
|
||||
|
@@ -1,11 +1,14 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manager(app: App, load_plugin):
|
||||
from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers
|
||||
|
||||
default_provider = matchers.provider
|
||||
matchers.set_provider(DEFAULT_PROVIDER_CLASS)
|
||||
assert matchers.provider == default_provider
|
||||
async def test_manager(app: App):
|
||||
try:
|
||||
default_provider = matchers.provider
|
||||
matchers.set_provider(DEFAULT_PROVIDER_CLASS)
|
||||
assert default_provider == matchers.provider
|
||||
finally:
|
||||
matchers.provider = app.provider
|
||||
|
@@ -1,18 +1,50 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from utils import make_fake_event, make_fake_message
|
||||
from nonebot.params import (
|
||||
ArgParam,
|
||||
BotParam,
|
||||
EventParam,
|
||||
StateParam,
|
||||
DependParam,
|
||||
DefaultParam,
|
||||
MatcherParam,
|
||||
ExceptionParam,
|
||||
)
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
REGEX_STR,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_depend(app: App, load_plugin):
|
||||
from nonebot.params import DependParam
|
||||
async def test_depend(app: App):
|
||||
from plugins.param.param_depend import (
|
||||
ClassDependency,
|
||||
runned,
|
||||
depends,
|
||||
class_depend,
|
||||
test_depends,
|
||||
annotated_depend,
|
||||
annotated_class_depend,
|
||||
annotated_prior_depend,
|
||||
)
|
||||
|
||||
async with app.test_dependent(depends, allow_types=[DependParam]) as ctx:
|
||||
@@ -29,14 +61,28 @@ async def test_depend(app: App, load_plugin):
|
||||
|
||||
assert len(runned) == 2 and runned[0] == runned[1] == 1
|
||||
|
||||
runned.clear()
|
||||
|
||||
async with app.test_dependent(class_depend, allow_types=[DependParam]) as ctx:
|
||||
ctx.should_return(ClassDependency(x=1, y=2))
|
||||
|
||||
async with app.test_dependent(annotated_depend, allow_types=[DependParam]) as ctx:
|
||||
ctx.should_return(1)
|
||||
|
||||
async with app.test_dependent(
|
||||
annotated_prior_depend, allow_types=[DependParam]
|
||||
) as ctx:
|
||||
ctx.should_return(1)
|
||||
assert runned == [1, 1]
|
||||
|
||||
async with app.test_dependent(
|
||||
annotated_class_depend, allow_types=[DependParam]
|
||||
) as ctx:
|
||||
ctx.should_return(ClassDependency(x=1, y=2))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot(app: App, load_plugin):
|
||||
from nonebot.params import BotParam
|
||||
from nonebot.exception import TypeMisMatch
|
||||
async def test_bot(app: App):
|
||||
from plugins.param.param_bot import (
|
||||
FooBot,
|
||||
get_bot,
|
||||
@@ -82,9 +128,7 @@ async def test_bot(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_event(app: App, load_plugin):
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from nonebot.params import EventParam, DependParam
|
||||
async def test_event(app: App):
|
||||
from plugins.param.param_event import (
|
||||
FooEvent,
|
||||
event,
|
||||
@@ -159,30 +203,14 @@ async def test_event(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state(app: App, load_plugin):
|
||||
from nonebot.params import StateParam, DependParam
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
CMD_START_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
)
|
||||
async def test_state(app: App):
|
||||
from plugins.param.param_state import (
|
||||
state,
|
||||
command,
|
||||
keyword,
|
||||
endswith,
|
||||
fullmatch,
|
||||
regex_str,
|
||||
regex_dict,
|
||||
startswith,
|
||||
command_arg,
|
||||
@@ -192,6 +220,7 @@ async def test_state(app: App, load_plugin):
|
||||
command_start,
|
||||
regex_matched,
|
||||
not_legacy_state,
|
||||
command_whitespace,
|
||||
shell_command_args,
|
||||
shell_command_argv,
|
||||
)
|
||||
@@ -203,10 +232,12 @@ async def test_state(app: App, load_plugin):
|
||||
RAW_CMD_KEY: "/cmd",
|
||||
CMD_START_KEY: "/",
|
||||
CMD_ARG_KEY: fake_message,
|
||||
CMD_WHITESPACE_KEY: " ",
|
||||
},
|
||||
SHELL_ARGV: ["-h"],
|
||||
SHELL_ARGS: {"help": True},
|
||||
REGEX_MATCHED: "[cq:test,arg=value]",
|
||||
REGEX_STR: "[cq:test,arg=value]",
|
||||
REGEX_GROUP: ("test", "arg=value"),
|
||||
REGEX_DICT: {"type": "test", "arg": "value"},
|
||||
STARTSWITH_KEY: "startswith",
|
||||
@@ -253,6 +284,12 @@ async def test_state(app: App, load_plugin):
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[PREFIX_KEY][CMD_START_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
command_whitespace, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[PREFIX_KEY][CMD_WHITESPACE_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
shell_command_argv, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
@@ -271,6 +308,12 @@ async def test_state(app: App, load_plugin):
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[REGEX_MATCHED])
|
||||
|
||||
async with app.test_dependent(
|
||||
regex_str, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[REGEX_STR])
|
||||
|
||||
async with app.test_dependent(
|
||||
regex_group, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
@@ -309,9 +352,7 @@ async def test_state(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App, load_plugin):
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import DependParam, MatcherParam
|
||||
async def test_matcher(app: App):
|
||||
from plugins.param.param_matcher import matcher, receive, last_receive
|
||||
|
||||
fake_matcher = Matcher()
|
||||
@@ -339,9 +380,7 @@ async def test_matcher(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_arg(app: App, load_plugin):
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.params import ArgParam
|
||||
async def test_arg(app: App):
|
||||
from plugins.param.param_arg import arg, arg_str, arg_plain_text
|
||||
|
||||
matcher = Matcher()
|
||||
@@ -362,8 +401,7 @@ async def test_arg(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exception(app: App, load_plugin):
|
||||
from nonebot.params import ExceptionParam
|
||||
async def test_exception(app: App):
|
||||
from plugins.param.param_exception import exc
|
||||
|
||||
exception = ValueError("test")
|
||||
@@ -373,8 +411,7 @@ async def test_exception(app: App, load_plugin):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default(app: App, load_plugin):
|
||||
from nonebot.params import DefaultParam
|
||||
async def test_default(app: App):
|
||||
from plugins.param.param_default import default
|
||||
|
||||
async with app.test_dependent(default, allow_types=[DefaultParam]) as ctx:
|
||||
|
@@ -4,13 +4,26 @@ import pytest
|
||||
from nonebug import App
|
||||
|
||||
from utils import make_fake_event
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.permission import (
|
||||
USER,
|
||||
NOTICE,
|
||||
MESSAGE,
|
||||
REQUEST,
|
||||
METAEVENT,
|
||||
SUPERUSER,
|
||||
User,
|
||||
Notice,
|
||||
Message,
|
||||
Request,
|
||||
MetaEvent,
|
||||
SuperUser,
|
||||
Permission,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission(app: App):
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.exception import SkippedException
|
||||
|
||||
async def falsy():
|
||||
return False
|
||||
|
||||
@@ -42,20 +55,8 @@ async def test_permission(app: App):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,expected",
|
||||
[
|
||||
("message", True),
|
||||
("notice", False),
|
||||
],
|
||||
)
|
||||
async def test_message(
|
||||
app: App,
|
||||
type: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import MESSAGE, Message
|
||||
|
||||
@pytest.mark.parametrize("type, expected", [("message", True), ("notice", False)])
|
||||
async def test_message(type: str, expected: bool):
|
||||
dependent = list(MESSAGE.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
@@ -66,20 +67,8 @@ async def test_message(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,expected",
|
||||
[
|
||||
("message", False),
|
||||
("notice", True),
|
||||
],
|
||||
)
|
||||
async def test_notice(
|
||||
app: App,
|
||||
type: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import NOTICE, Notice
|
||||
|
||||
@pytest.mark.parametrize("type, expected", [("message", False), ("notice", True)])
|
||||
async def test_notice(type: str, expected: bool):
|
||||
dependent = list(NOTICE.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
@@ -90,20 +79,8 @@ async def test_notice(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,expected",
|
||||
[
|
||||
("message", False),
|
||||
("request", True),
|
||||
],
|
||||
)
|
||||
async def test_request(
|
||||
app: App,
|
||||
type: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import REQUEST, Request
|
||||
|
||||
@pytest.mark.parametrize("type, expected", [("message", False), ("request", True)])
|
||||
async def test_request(type: str, expected: bool):
|
||||
dependent = list(REQUEST.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
@@ -114,20 +91,8 @@ async def test_request(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,expected",
|
||||
[
|
||||
("message", False),
|
||||
("meta_event", True),
|
||||
],
|
||||
)
|
||||
async def test_metaevent(
|
||||
app: App,
|
||||
type: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import METAEVENT, MetaEvent
|
||||
|
||||
@pytest.mark.parametrize("type, expected", [("message", False), ("meta_event", True)])
|
||||
async def test_metaevent(type: str, expected: bool):
|
||||
dependent = list(METAEVENT.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
@@ -139,7 +104,7 @@ async def test_metaevent(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,user_id,expected",
|
||||
"type, user_id, expected",
|
||||
[
|
||||
("message", "test", True),
|
||||
("message", "foo", False),
|
||||
@@ -148,14 +113,7 @@ async def test_metaevent(
|
||||
("notice", "test", True),
|
||||
],
|
||||
)
|
||||
async def test_superuser(
|
||||
app: App,
|
||||
type: str,
|
||||
user_id: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import SUPERUSER, SuperUser
|
||||
|
||||
async def test_superuser(app: App, type: str, user_id: str, expected: bool):
|
||||
dependent = list(SUPERUSER.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
@@ -170,7 +128,7 @@ async def test_superuser(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"session_ids,session_id,expected",
|
||||
"session_ids, session_id, expected",
|
||||
[
|
||||
(("user", "foo"), "user", True),
|
||||
(("user", "foo"), "bar", False),
|
||||
@@ -180,8 +138,6 @@ async def test_superuser(
|
||||
async def test_user(
|
||||
app: App, session_ids: Tuple[str, ...], session_id: Optional[str], expected: bool
|
||||
):
|
||||
from nonebot.permission import USER, User
|
||||
|
||||
dependent = list(USER(*session_ids).checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
|
@@ -1,16 +1,11 @@
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
|
||||
async def test_get_plugin():
|
||||
# check simple plugin
|
||||
plugin = nonebot.get_plugin("export")
|
||||
assert plugin
|
||||
@@ -28,12 +23,15 @@ async def test_get_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_available_plugin(app: App):
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
async def test_get_available_plugin():
|
||||
old_managers = _managers.copy()
|
||||
_managers.clear()
|
||||
try:
|
||||
_managers.append(PluginManager(["plugins.export", "plugin.require"]))
|
||||
|
||||
_managers.append(PluginManager(["plugins.export", "plugin.require"]))
|
||||
|
||||
# check get available plugins
|
||||
plugin_names = nonebot.get_available_plugin_names()
|
||||
assert plugin_names == {"export", "require"}
|
||||
# check get available plugins
|
||||
plugin_names = nonebot.get_available_plugin_names()
|
||||
assert plugin_names == {"export", "require"}
|
||||
finally:
|
||||
_managers.clear()
|
||||
_managers.extend(old_managers)
|
||||
|
@@ -1,38 +1,32 @@
|
||||
import sys
|
||||
from typing import Set
|
||||
from pathlib import Path
|
||||
from dataclasses import asdict
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
import nonebot
|
||||
from nonebot.plugin import Plugin, PluginManager, _managers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugin(app: App):
|
||||
import nonebot
|
||||
|
||||
async def test_load_plugin():
|
||||
# check regular
|
||||
assert nonebot.load_plugin("plugins.metadata")
|
||||
assert nonebot.load_plugin("dynamic.simple")
|
||||
|
||||
# check path
|
||||
assert nonebot.load_plugin(Path("plugins/export"))
|
||||
assert nonebot.load_plugin(Path("dynamic/path.py"))
|
||||
|
||||
# check not found
|
||||
assert nonebot.load_plugin("some_plugin_not_exist") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugins(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager
|
||||
|
||||
async def test_load_plugins(load_plugin: Set[Plugin], load_example: Set[Plugin]):
|
||||
loaded_plugins = {
|
||||
plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin
|
||||
}
|
||||
assert loaded_plugins == load_plugin
|
||||
assert loaded_plugins >= load_plugin | load_example
|
||||
|
||||
# check simple plugin
|
||||
assert "plugins.export" in sys.modules
|
||||
@@ -51,9 +45,7 @@ async def test_load_plugins(app: App, load_plugin: Set["Plugin"]):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
|
||||
async def test_load_nested_plugin():
|
||||
parent_plugin = nonebot.get_plugin("nested")
|
||||
sub_plugin = nonebot.get_plugin("nested_subplugin")
|
||||
sub_plugin2 = nonebot.get_plugin("nested_subplugin2")
|
||||
@@ -64,9 +56,7 @@ async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_json(app: App):
|
||||
import nonebot
|
||||
|
||||
async def test_load_json():
|
||||
nonebot.load_from_json("./plugins.json")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
@@ -74,9 +64,7 @@ async def test_load_json(app: App):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_toml(app: App):
|
||||
import nonebot
|
||||
|
||||
async def test_load_toml():
|
||||
nonebot.load_from_toml("./plugins.toml")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
@@ -87,35 +75,27 @@ async def test_load_toml(app: App):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bad_plugin(app: App):
|
||||
import nonebot
|
||||
|
||||
async def test_bad_plugin():
|
||||
nonebot.load_plugins("bad_plugins")
|
||||
|
||||
assert nonebot.get_plugin("bad_plugins") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
import nonebot
|
||||
|
||||
async def test_require_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||
def _patched_find(name: str):
|
||||
assert False
|
||||
pytest.fail("require existing plugin should not call find_manager_by_name")
|
||||
|
||||
monkeypatch.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find)
|
||||
|
||||
nonebot.load_plugin("plugins.export")
|
||||
|
||||
nonebot.require("plugins.export")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
|
||||
m = PluginManager(["plugins.export"])
|
||||
async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||
m = PluginManager(["dynamic.require_not_loaded"])
|
||||
_managers.append(m)
|
||||
num_managers = len(_managers)
|
||||
|
||||
origin_load = PluginManager.load_plugin
|
||||
|
||||
@@ -125,33 +105,29 @@ async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
monkeypatch.setattr(PluginManager, "load_plugin", _patched_load)
|
||||
|
||||
nonebot.require("plugins.export")
|
||||
nonebot.require("dynamic.require_not_loaded")
|
||||
|
||||
assert len(_managers) == 1
|
||||
assert len(_managers) == num_managers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_declared(app: App):
|
||||
import nonebot
|
||||
from nonebot.plugin import _managers
|
||||
async def test_require_not_declared():
|
||||
num_managers = len(_managers)
|
||||
|
||||
nonebot.require("plugins.export")
|
||||
nonebot.require("dynamic.require_not_declared")
|
||||
|
||||
assert len(_managers) == 1
|
||||
assert _managers[-1].plugins == {"plugins.export"}
|
||||
assert len(_managers) == num_managers + 1
|
||||
assert _managers[-1].plugins == {"dynamic.require_not_declared"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_found(app: App):
|
||||
import nonebot
|
||||
|
||||
async def test_require_not_found():
|
||||
with pytest.raises(RuntimeError):
|
||||
nonebot.require("some_plugin_not_exist")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_metadata(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
async def test_plugin_metadata():
|
||||
from plugins.metadata import Config
|
||||
|
||||
plugin = nonebot.get_plugin("metadata")
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.plugin import PluginManager
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugin_name(app: App):
|
||||
from nonebot.plugin import PluginManager
|
||||
|
||||
m = PluginManager(plugins=["plugins.export"])
|
||||
module1 = m.load_plugin("export")
|
||||
module2 = m.load_plugin("plugins.export")
|
||||
async def test_load_plugin_name():
|
||||
m = PluginManager(plugins=["dynamic.manager"])
|
||||
module1 = m.load_plugin("manager")
|
||||
module2 = m.load_plugin("dynamic.manager")
|
||||
assert module1 is module2
|
||||
|
@@ -1,116 +1,149 @@
|
||||
from typing import Type, Optional
|
||||
from typing import Type, Callable, Optional
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
import nonebot
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.typing import T_RuleChecker
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from nonebot.rule import (
|
||||
RegexRule,
|
||||
IsTypeRule,
|
||||
CommandRule,
|
||||
EndswithRule,
|
||||
KeywordsRule,
|
||||
FullmatchRule,
|
||||
StartswithRule,
|
||||
ShellCommandRule,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on(app: App, load_plugin):
|
||||
import nonebot
|
||||
@pytest.mark.parametrize(
|
||||
"matcher_name, pre_rule_factory, has_permission",
|
||||
[
|
||||
pytest.param("matcher_on", None, True),
|
||||
pytest.param("matcher_on_metaevent", None, False),
|
||||
pytest.param("matcher_on_message", None, True),
|
||||
pytest.param("matcher_on_notice", None, False),
|
||||
pytest.param("matcher_on_request", None, False),
|
||||
pytest.param(
|
||||
"matcher_on_startswith", lambda e: StartswithRule(("test",)), True
|
||||
),
|
||||
pytest.param("matcher_on_endswith", lambda e: EndswithRule(("test",)), True),
|
||||
pytest.param("matcher_on_fullmatch", lambda e: FullmatchRule(("test",)), True),
|
||||
pytest.param("matcher_on_keyword", lambda e: KeywordsRule("test"), True),
|
||||
pytest.param("matcher_on_command", lambda e: CommandRule([("test",)]), True),
|
||||
pytest.param(
|
||||
"matcher_on_shell_command",
|
||||
lambda e: ShellCommandRule([("test",)], None),
|
||||
True,
|
||||
),
|
||||
pytest.param("matcher_on_regex", lambda e: RegexRule("test"), True),
|
||||
pytest.param("matcher_on_type", lambda e: IsTypeRule(e), True),
|
||||
pytest.param("matcher_sub_cmd", lambda e: CommandRule([("test", "sub")]), True),
|
||||
pytest.param(
|
||||
"matcher_sub_shell_cmd",
|
||||
lambda e: ShellCommandRule([("test", "sub")], None),
|
||||
True,
|
||||
),
|
||||
pytest.param("matcher_group_on", None, True),
|
||||
pytest.param("matcher_group_on_metaevent", None, False),
|
||||
pytest.param("matcher_group_on_message", None, True),
|
||||
pytest.param("matcher_group_on_notice", None, False),
|
||||
pytest.param("matcher_group_on_request", None, False),
|
||||
pytest.param(
|
||||
"matcher_group_on_startswith",
|
||||
lambda e: StartswithRule(("test",)),
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"matcher_group_on_endswith",
|
||||
lambda e: EndswithRule(("test",)),
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"matcher_group_on_fullmatch",
|
||||
lambda e: FullmatchRule(("test",)),
|
||||
True,
|
||||
),
|
||||
pytest.param("matcher_group_on_keyword", lambda e: KeywordsRule("test"), True),
|
||||
pytest.param(
|
||||
"matcher_group_on_command",
|
||||
lambda e: CommandRule([("test",)]),
|
||||
True,
|
||||
),
|
||||
pytest.param(
|
||||
"matcher_group_on_shell_command",
|
||||
lambda e: ShellCommandRule([("test",)], None),
|
||||
True,
|
||||
),
|
||||
pytest.param("matcher_group_on_regex", lambda e: RegexRule("test"), True),
|
||||
pytest.param("matcher_group_on_type", lambda e: IsTypeRule(e), True),
|
||||
],
|
||||
)
|
||||
async def test_on(
|
||||
matcher_name: str,
|
||||
pre_rule_factory: Optional[Callable[[Type[Event]], T_RuleChecker]],
|
||||
has_permission: bool,
|
||||
):
|
||||
import plugins.plugin.matchers as module
|
||||
from nonebot.typing import T_RuleChecker
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from nonebot.rule import (
|
||||
RegexRule,
|
||||
IsTypeRule,
|
||||
CommandRule,
|
||||
EndswithRule,
|
||||
KeywordsRule,
|
||||
FullmatchRule,
|
||||
StartswithRule,
|
||||
ShellCommandRule,
|
||||
)
|
||||
from plugins.plugin.matchers import (
|
||||
TestEvent,
|
||||
rule,
|
||||
state,
|
||||
handler,
|
||||
priority,
|
||||
matcher_on,
|
||||
permission,
|
||||
expire_time,
|
||||
matcher_on_type,
|
||||
matcher_sub_cmd,
|
||||
matcher_group_on,
|
||||
matcher_on_regex,
|
||||
matcher_on_notice,
|
||||
matcher_on_command,
|
||||
matcher_on_keyword,
|
||||
matcher_on_message,
|
||||
matcher_on_request,
|
||||
matcher_on_endswith,
|
||||
matcher_on_fullmatch,
|
||||
matcher_on_metaevent,
|
||||
matcher_group_on_type,
|
||||
matcher_on_startswith,
|
||||
matcher_sub_shell_cmd,
|
||||
matcher_group_on_regex,
|
||||
matcher_group_on_notice,
|
||||
matcher_group_on_command,
|
||||
matcher_group_on_keyword,
|
||||
matcher_group_on_message,
|
||||
matcher_group_on_request,
|
||||
matcher_on_shell_command,
|
||||
matcher_group_on_endswith,
|
||||
matcher_group_on_fullmatch,
|
||||
matcher_group_on_metaevent,
|
||||
matcher_group_on_startswith,
|
||||
matcher_group_on_shell_command,
|
||||
)
|
||||
|
||||
matcher = getattr(module, matcher_name)
|
||||
assert issubclass(matcher, Matcher), f"{matcher_name} should be a Matcher"
|
||||
|
||||
pre_rule = pre_rule_factory(TestEvent) if pre_rule_factory else None
|
||||
|
||||
plugin = nonebot.get_plugin("plugin")
|
||||
assert plugin, "plugin should be loaded"
|
||||
|
||||
def _check(
|
||||
matcher: Type[Matcher],
|
||||
pre_rule: Optional[T_RuleChecker],
|
||||
has_permission: bool,
|
||||
):
|
||||
assert {dependent.call for dependent in matcher.rule.checkers} == (
|
||||
{pre_rule, rule} if pre_rule else {rule}
|
||||
)
|
||||
if has_permission:
|
||||
assert {dependent.call for dependent in matcher.permission.checkers} == {
|
||||
permission
|
||||
}
|
||||
else:
|
||||
assert not matcher.permission.checkers
|
||||
assert [dependent.call for dependent in matcher.handlers] == [handler]
|
||||
assert matcher.temp is True
|
||||
assert matcher.expire_time == expire_time
|
||||
assert matcher in matchers[priority]
|
||||
assert matcher.block is True
|
||||
assert matcher._default_state == state
|
||||
assert {dependent.call for dependent in matcher.rule.checkers} == (
|
||||
{pre_rule, rule} if pre_rule else {rule}
|
||||
)
|
||||
if has_permission:
|
||||
assert {dependent.call for dependent in matcher.permission.checkers} == {
|
||||
permission
|
||||
}
|
||||
else:
|
||||
assert not matcher.permission.checkers
|
||||
assert [dependent.call for dependent in matcher.handlers] == [handler]
|
||||
assert matcher.temp is True
|
||||
assert matcher.expire_time == expire_time
|
||||
assert matcher in matchers[priority]
|
||||
assert matcher.block is True
|
||||
assert matcher._default_state == state
|
||||
|
||||
assert matcher.plugin is plugin
|
||||
assert matcher in plugin.matcher
|
||||
assert matcher.module is module
|
||||
assert matcher.plugin_name == "plugin"
|
||||
assert matcher.module_name == "plugins.plugin.matchers"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_on():
|
||||
import plugins.plugin.matchers as module
|
||||
from plugins.plugin.matchers import matcher_on_factory
|
||||
|
||||
matcher = matcher_on_factory()
|
||||
|
||||
plugin = nonebot.get_plugin("plugin")
|
||||
assert plugin, "plugin should be loaded"
|
||||
|
||||
try:
|
||||
assert matcher.plugin is plugin
|
||||
assert matcher not in plugin.matcher
|
||||
assert matcher.module is module
|
||||
assert matcher.plugin_name == "plugin"
|
||||
assert matcher.module_name == "plugins.plugin.matchers"
|
||||
|
||||
_check(matcher_on, None, True)
|
||||
_check(matcher_on_metaevent, None, False)
|
||||
_check(matcher_on_message, None, True)
|
||||
_check(matcher_on_notice, None, False)
|
||||
_check(matcher_on_request, None, False)
|
||||
_check(matcher_on_startswith, StartswithRule(("test",)), True)
|
||||
_check(matcher_on_endswith, EndswithRule(("test",)), True)
|
||||
_check(matcher_on_fullmatch, FullmatchRule(("test",)), True)
|
||||
_check(matcher_on_keyword, KeywordsRule("test"), True)
|
||||
_check(matcher_on_command, CommandRule([("test",)]), True)
|
||||
_check(matcher_on_shell_command, ShellCommandRule([("test",)], None), True)
|
||||
_check(matcher_on_regex, RegexRule("test"), True)
|
||||
_check(matcher_on_type, IsTypeRule(TestEvent), True)
|
||||
_check(matcher_sub_cmd, CommandRule([("test", "sub")]), True)
|
||||
_check(matcher_sub_shell_cmd, ShellCommandRule([("test", "sub")], None), True)
|
||||
_check(matcher_group_on, None, True)
|
||||
_check(matcher_group_on_metaevent, None, False)
|
||||
_check(matcher_group_on_message, None, True)
|
||||
_check(matcher_group_on_notice, None, False)
|
||||
_check(matcher_group_on_request, None, False)
|
||||
_check(matcher_group_on_startswith, StartswithRule(("test",)), True)
|
||||
_check(matcher_group_on_endswith, EndswithRule(("test",)), True)
|
||||
_check(matcher_group_on_fullmatch, FullmatchRule(("test",)), True)
|
||||
_check(matcher_group_on_keyword, KeywordsRule("test"), True)
|
||||
_check(matcher_group_on_command, CommandRule([("test",)]), True)
|
||||
_check(matcher_group_on_shell_command, ShellCommandRule([("test",)], None), True)
|
||||
_check(matcher_group_on_regex, RegexRule("test"), True)
|
||||
_check(matcher_group_on_type, IsTypeRule(TestEvent), True)
|
||||
finally:
|
||||
matcher.destroy()
|
||||
|
@@ -4,14 +4,55 @@ from typing import Dict, Tuple, Union, Optional
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.typing import T_State
|
||||
from utils import make_fake_event, make_fake_message
|
||||
from nonebot.exception import ParserExit, SkippedException
|
||||
from nonebot.consts import (
|
||||
CMD_KEY,
|
||||
REGEX_STR,
|
||||
PREFIX_KEY,
|
||||
REGEX_DICT,
|
||||
SHELL_ARGS,
|
||||
SHELL_ARGV,
|
||||
CMD_ARG_KEY,
|
||||
KEYWORD_KEY,
|
||||
REGEX_GROUP,
|
||||
ENDSWITH_KEY,
|
||||
FULLMATCH_KEY,
|
||||
REGEX_MATCHED,
|
||||
STARTSWITH_KEY,
|
||||
CMD_WHITESPACE_KEY,
|
||||
)
|
||||
from nonebot.rule import (
|
||||
CMD_RESULT,
|
||||
TRIE_VALUE,
|
||||
Rule,
|
||||
ToMeRule,
|
||||
TrieRule,
|
||||
Namespace,
|
||||
RegexRule,
|
||||
IsTypeRule,
|
||||
CommandRule,
|
||||
EndswithRule,
|
||||
KeywordsRule,
|
||||
FullmatchRule,
|
||||
ArgumentParser,
|
||||
StartswithRule,
|
||||
ShellCommandRule,
|
||||
regex,
|
||||
to_me,
|
||||
command,
|
||||
is_type,
|
||||
keyword,
|
||||
endswith,
|
||||
fullmatch,
|
||||
startswith,
|
||||
shell_command,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rule(app: App):
|
||||
from nonebot.rule import Rule
|
||||
from nonebot.exception import SkippedException
|
||||
|
||||
async def falsy():
|
||||
return False
|
||||
|
||||
@@ -42,9 +83,47 @@ async def test_rule(app: App):
|
||||
assert await Rule(truthy, skipped)(bot, event, {}) == False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trie(app: App):
|
||||
TrieRule.add_prefix("/fake-prefix", TRIE_VALUE("/", ("fake-prefix",)))
|
||||
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
message = Message("/fake-prefix some args")
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {}
|
||||
TrieRule.get_value(bot, event, state)
|
||||
assert state[PREFIX_KEY] == CMD_RESULT(
|
||||
command=("fake-prefix",),
|
||||
raw_command="/fake-prefix",
|
||||
command_arg=Message("some args"),
|
||||
command_start="/",
|
||||
command_whitespace=" ",
|
||||
)
|
||||
|
||||
message = MessageSegment.text("/fake-prefix ") + MessageSegment.image(
|
||||
"fake url"
|
||||
)
|
||||
event = make_fake_event(_message=message)()
|
||||
state = {}
|
||||
TrieRule.get_value(bot, event, state)
|
||||
assert state[PREFIX_KEY] == CMD_RESULT(
|
||||
command=("fake-prefix",),
|
||||
raw_command="/fake-prefix",
|
||||
command_arg=Message(MessageSegment.image("fake url")),
|
||||
command_start="/",
|
||||
command_whitespace=" ",
|
||||
)
|
||||
|
||||
del TrieRule.prefix["/fake-prefix"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"msg,ignorecase,type,text,expected",
|
||||
"msg, ignorecase, type, text, expected",
|
||||
[
|
||||
("prefix", False, "message", "prefix_", True),
|
||||
("prefix", False, "message", "Prefix_", False),
|
||||
@@ -54,20 +133,17 @@ async def test_rule(app: App):
|
||||
("prefix", False, "message", "fooprefix", False),
|
||||
("prefix", False, "message", None, False),
|
||||
(("prefix", "foo"), False, "message", "fooprefix", True),
|
||||
("prefix", False, "notice", "prefix", True),
|
||||
("prefix", False, "notice", "foo", False),
|
||||
],
|
||||
)
|
||||
async def test_startswith(
|
||||
app: App,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
ignorecase: bool,
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.consts import STARTSWITH_KEY
|
||||
from nonebot.rule import StartswithRule, startswith
|
||||
|
||||
test_startswith = startswith(msg, ignorecase)
|
||||
dependent = list(test_startswith.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -87,7 +163,7 @@ async def test_startswith(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"msg,ignorecase,type,text,expected",
|
||||
"msg, ignorecase, type, text, expected",
|
||||
[
|
||||
("suffix", False, "message", "_suffix", True),
|
||||
("suffix", False, "message", "_Suffix", False),
|
||||
@@ -97,20 +173,17 @@ async def test_startswith(
|
||||
("suffix", False, "message", "suffixfoo", False),
|
||||
("suffix", False, "message", None, False),
|
||||
(("suffix", "foo"), False, "message", "suffixfoo", True),
|
||||
("suffix", False, "notice", "suffix", True),
|
||||
("suffix", False, "notice", "foo", False),
|
||||
],
|
||||
)
|
||||
async def test_endswith(
|
||||
app: App,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
ignorecase: bool,
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.consts import ENDSWITH_KEY
|
||||
from nonebot.rule import EndswithRule, endswith
|
||||
|
||||
test_endswith = endswith(msg, ignorecase)
|
||||
dependent = list(test_endswith.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -130,7 +203,7 @@ async def test_endswith(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"msg,ignorecase,type,text,expected",
|
||||
"msg, ignorecase, type, text, expected",
|
||||
[
|
||||
("fullmatch", False, "message", "fullmatch", True),
|
||||
("fullmatch", False, "message", "Fullmatch", False),
|
||||
@@ -140,20 +213,17 @@ async def test_endswith(
|
||||
("fullmatch", False, "message", "_fullmatch_", False),
|
||||
("fullmatch", False, "message", None, False),
|
||||
(("fullmatch", "foo"), False, "message", "fullmatchfoo", False),
|
||||
("fullmatch", False, "notice", "fullmatch", True),
|
||||
("fullmatch", False, "notice", "foo", False),
|
||||
],
|
||||
)
|
||||
async def test_fullmatch(
|
||||
app: App,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
ignorecase: bool,
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.consts import FULLMATCH_KEY
|
||||
from nonebot.rule import FullmatchRule, fullmatch
|
||||
|
||||
test_fullmatch = fullmatch(msg, ignorecase)
|
||||
dependent = list(test_fullmatch.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -173,25 +243,22 @@ async def test_fullmatch(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"kws,type,text,expected",
|
||||
"kws, type, text, expected",
|
||||
[
|
||||
(("key",), "message", "_key_", True),
|
||||
(("key", "foo"), "message", "_foo_", True),
|
||||
(("key",), "message", None, False),
|
||||
(("key",), "notice", "foo", False),
|
||||
(("key",), "message", "foo", False),
|
||||
(("key",), "notice", "_key_", True),
|
||||
(("key",), "notice", "foo", False),
|
||||
],
|
||||
)
|
||||
async def test_keyword(
|
||||
app: App,
|
||||
kws: Tuple[str, ...],
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.consts import KEYWORD_KEY
|
||||
from nonebot.rule import KeywordsRule, keyword
|
||||
|
||||
test_keyword = keyword(*kws)
|
||||
dependent = list(test_keyword.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -208,31 +275,37 @@ async def test_keyword(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"cmds", [(("help",),), (("help", "foo"),), (("help",), ("foo",))]
|
||||
"cmds, cmd, force_whitespace, whitespace, expected",
|
||||
[
|
||||
[(("help",),), ("help",), None, None, True],
|
||||
[(("help",),), ("foo",), None, None, False],
|
||||
[(("help", "foo"),), ("help", "foo"), True, " ", True],
|
||||
[(("help",), ("foo",)), ("help",), " ", " ", True],
|
||||
[(("help",),), ("help",), False, " ", False],
|
||||
[(("help",),), ("help",), True, None, False],
|
||||
[(("help",),), ("help",), "\n", " ", False],
|
||||
],
|
||||
)
|
||||
async def test_command(app: App, cmds: Tuple[Tuple[str, ...]]):
|
||||
from nonebot.rule import CommandRule, command
|
||||
from nonebot.consts import CMD_KEY, PREFIX_KEY
|
||||
|
||||
test_command = command(*cmds)
|
||||
async def test_command(
|
||||
cmds: Tuple[Tuple[str, ...]],
|
||||
cmd: Tuple[str, ...],
|
||||
force_whitespace: Optional[Union[str, bool]],
|
||||
whitespace: Optional[str],
|
||||
expected: bool,
|
||||
):
|
||||
test_command = command(*cmds, force_whitespace=force_whitespace)
|
||||
dependent = list(test_command.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, CommandRule)
|
||||
assert checker.cmds == cmds
|
||||
|
||||
for cmd in cmds:
|
||||
state = {PREFIX_KEY: {CMD_KEY: cmd}}
|
||||
assert await dependent(state=state)
|
||||
state = {PREFIX_KEY: {CMD_KEY: cmd, CMD_WHITESPACE_KEY: whitespace}}
|
||||
assert await dependent(state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shell_command(app: App):
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.exception import ParserExit
|
||||
from nonebot.consts import CMD_KEY, PREFIX_KEY, SHELL_ARGS, SHELL_ARGV, CMD_ARG_KEY
|
||||
from nonebot.rule import Namespace, ArgumentParser, ShellCommandRule, shell_command
|
||||
|
||||
async def test_shell_command():
|
||||
state: T_State
|
||||
CMD = ("test",)
|
||||
Message = make_fake_message()
|
||||
@@ -296,6 +369,7 @@ async def test_shell_command(app: App):
|
||||
assert state[SHELL_ARGV] == []
|
||||
assert isinstance(state[SHELL_ARGS], ParserExit)
|
||||
assert state[SHELL_ARGS].status != 0
|
||||
assert state[SHELL_ARGS].message.startswith(parser.format_usage() + "test: error:")
|
||||
|
||||
test_message_parser = shell_command(CMD, parser=parser)
|
||||
dependent = list(test_message_parser.checkers)[0]
|
||||
@@ -327,7 +401,7 @@ async def test_shell_command(app: App):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"pattern,type,text,expected,matched,group,dict",
|
||||
"pattern, type, text, expected, matched, string, group, dict",
|
||||
[
|
||||
(
|
||||
r"(?P<key>key\d)",
|
||||
@@ -335,27 +409,25 @@ async def test_shell_command(app: App):
|
||||
"_key1_",
|
||||
True,
|
||||
"key1",
|
||||
"key1",
|
||||
("key1",),
|
||||
{"key": "key1"},
|
||||
),
|
||||
(r"foo", "message", None, False, None, None, None),
|
||||
(r"foo", "notice", "foo", False, None, None, None),
|
||||
(r"foo", "message", None, False, None, None, None, None),
|
||||
(r"foo", "notice", "foo", True, "foo", "foo", tuple(), {}),
|
||||
(r"foo", "notice", "bar", False, None, None, None, None),
|
||||
],
|
||||
)
|
||||
async def test_regex(
|
||||
app: App,
|
||||
pattern: str,
|
||||
type: str,
|
||||
text: Optional[str],
|
||||
expected: bool,
|
||||
matched: Optional[str],
|
||||
string: Optional[str],
|
||||
group: Optional[Tuple[str, ...]],
|
||||
dict: Optional[Dict[str, str]],
|
||||
):
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.rule import RegexRule, regex
|
||||
from nonebot.consts import REGEX_DICT, REGEX_GROUP, REGEX_MATCHED
|
||||
|
||||
test_regex = regex(pattern)
|
||||
dependent = list(test_regex.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -368,15 +440,14 @@ async def test_regex(
|
||||
state = {}
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
assert state.get(REGEX_MATCHED) == matched
|
||||
assert state.get(REGEX_STR) == string
|
||||
assert state.get(REGEX_GROUP) == group
|
||||
assert state.get(REGEX_DICT) == dict
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("expected", [True, False])
|
||||
async def test_to_me(app: App, expected: bool):
|
||||
from nonebot.rule import ToMeRule, to_me
|
||||
|
||||
async def test_to_me(expected: bool):
|
||||
test_to_me = to_me()
|
||||
dependent = list(test_to_me.checkers)[0]
|
||||
checker = dependent.call
|
||||
@@ -388,9 +459,7 @@ async def test_to_me(app: App, expected: bool):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_type(app: App):
|
||||
from nonebot.rule import IsTypeRule, is_type
|
||||
|
||||
async def test_is_type():
|
||||
Event1 = make_fake_event()
|
||||
Event2 = make_fake_event()
|
||||
Event3 = make_fake_event()
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import json
|
||||
|
||||
from utils import make_fake_message
|
||||
from nonebot.utils import DataclassEncoder
|
||||
|
||||
|
||||
def test_dataclass_encoder():
|
||||
from nonebot.utils import DataclassEncoder
|
||||
|
||||
simple = json.dumps("123", cls=DataclassEncoder)
|
||||
assert simple == '"123"'
|
||||
|
||||
|
@@ -1,9 +1,8 @@
|
||||
from typing import TYPE_CHECKING, Type, Union, Mapping, Iterable, Optional
|
||||
from typing import Type, Union, Mapping, Iterable, Optional
|
||||
|
||||
from pydantic import create_model
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.adapters import Event, Message
|
||||
from nonebot.adapters import Event, Message, MessageSegment
|
||||
|
||||
|
||||
def escape_text(s: str, *, escape_comma: bool = True) -> str:
|
||||
@@ -14,8 +13,6 @@ def escape_text(s: str, *, escape_comma: bool = True) -> str:
|
||||
|
||||
|
||||
def make_fake_message():
|
||||
from nonebot.adapters import Message, MessageSegment
|
||||
|
||||
class FakeMessageSegment(MessageSegment):
|
||||
@classmethod
|
||||
def get_message_class(cls):
|
||||
@@ -61,18 +58,16 @@ def make_fake_message():
|
||||
|
||||
|
||||
def make_fake_event(
|
||||
_base: Optional[Type["Event"]] = None,
|
||||
_base: Optional[Type[Event]] = None,
|
||||
_type: str = "message",
|
||||
_name: str = "test",
|
||||
_description: str = "test",
|
||||
_user_id: Optional[str] = "test",
|
||||
_session_id: Optional[str] = "test",
|
||||
_message: Optional["Message"] = None,
|
||||
_message: Optional[Message] = None,
|
||||
_to_me: bool = True,
|
||||
**fields,
|
||||
) -> Type["Event"]:
|
||||
from nonebot.adapters import Event
|
||||
|
||||
) -> Type[Event]:
|
||||
_Fake = create_model("_Fake", __base__=_base or Event, **fields)
|
||||
|
||||
class FakeEvent(_Fake):
|
||||
|
@@ -6,44 +6,44 @@ slug: /
|
||||
|
||||
# 概览
|
||||
|
||||
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架,它基于 Python 的类型注解和异步特性,能够为你的需求实现提供便捷灵活的支持。
|
||||
NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架(下称 NoneBot),它基于 Python 的类型注解和异步优先特性(兼容同步),能够为你的需求实现提供便捷灵活的支持。同时,NoneBot 拥有大量的开发者为其开发插件,用户无需编写任何代码,仅需完成环境配置及插件安装,就可以正常使用 NoneBot。
|
||||
|
||||
需要注意的是,NoneBot2 仅支持 **Python 3.8 以上版本**
|
||||
需要注意的是,NoneBot 仅支持 **Python 3.8 以上版本**
|
||||
|
||||
## 特色
|
||||
|
||||
### 异步优先
|
||||
|
||||
NoneBot2 基于 Python [asyncio](https://docs.python.org/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
|
||||
NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
|
||||
|
||||
### 完整的类型注解
|
||||
|
||||
NoneBot2 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 `pyright`/`pylance` 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./start/editor-support))。
|
||||
NoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 Pyright(Pylance) 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./editor-support))。
|
||||
|
||||
### 开箱即用
|
||||
|
||||
NoneBot2 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得初次接触 NoneBot2 时更容易上手。详细使用方法请参考各文档章节以及[使用脚手架](./start/nb-cli)。
|
||||
NoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。
|
||||
|
||||
### 插件系统
|
||||
|
||||
插件系统是 NoneBot2 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。
|
||||
插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。
|
||||
|
||||
### 依赖注入系统
|
||||
|
||||
NoneBot2 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。
|
||||
NoneBot 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。
|
||||
|
||||
#### 什么是依赖注入
|
||||
|
||||
[**“依赖注入”**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**“依赖”**。
|
||||
[**『依赖注入』**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**『依赖』**。
|
||||
|
||||
系统(在这里是指 NoneBot2)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**“注入”**依赖性)
|
||||
系统(在这里是指 NoneBot)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**『注入』**依赖性)
|
||||
|
||||
这在你有以下情形的需求时非常有用:
|
||||
|
||||
- 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复)
|
||||
- 共享数据库以及网络请求连接会话
|
||||
- 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session`
|
||||
- 用户权限检查以及认证
|
||||
- 机器人用户权限检查以及认证
|
||||
- 还有更多...
|
||||
|
||||
它在完成上述工作的同时,还能尽量减少代码的耦合和重复
|
||||
|
@@ -1,207 +0,0 @@
|
||||
---
|
||||
id: index
|
||||
sidebar_position: 0
|
||||
description: 深入了解 NoneBot2 运行机制
|
||||
slug: /advanced/
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 深入
|
||||
|
||||
:::danger 警告
|
||||
进阶部分尚未更新完成
|
||||
:::
|
||||
|
||||
## 它如何工作?
|
||||
|
||||
如同[概览](../README.md)所言:
|
||||
|
||||
> NoneBot2 是一个可扩展的 Python 异步机器人框架,它会对机器人收到的事件进行解析和处理,并以插件化的形式,按优先级分发给事件所对应的事件响应器,来完成具体的功能。
|
||||
|
||||
NoneBot2 是一个可以对机器人上报的事件进行处理并完成具体功能的机器人框架,在这里,我们将简要讲述它的工作内容。
|
||||
|
||||
**便捷起见,以下内容对 NoneBot2 会被称为 NoneBot,与 NoneBot2 交互的机器人实现会被称为协议端**。
|
||||
|
||||
在实际应用中,NoneBot 会充当一个高性能,轻量级的 Python 微服务框架。协议端可以通过 http、websocket 等方式与之通信,这个通信往往是双向的:一方面,协议端可以上报数据给 NoneBot,NoneBot 会处理数据并返回响应给协议端;另一方面,NoneBot 可以主动推送数据给协议端。而 NoneBot 便是围绕双向通信进行工作的。
|
||||
|
||||
在开始工作之前,NoneBot 需要进行准备工作:
|
||||
|
||||
1. **运行 `nonebot.init` 初始化函数**,它会读取配置文件,并初始化 NoneBot 和后端驱动 `Driver` 对象。
|
||||
2. **注册协议适配器 `Adapter`**。
|
||||
3. **加载插件**。
|
||||
|
||||
准备工作完成后,NoneBot 会利用 uvicorn 启动,并运行 `on_startup` 钩子函数。
|
||||
|
||||
随后,倘若一个协议端与 NoneBot 进行了连接,NoneBot 的后端驱动 `Driver` 就会将数据交给 `Adapter`,然后会实例化 `Bot`,NoneBot 便会利用 `Bot` 开始工作,它的工作内容分为两个方面:
|
||||
|
||||
1. **事件处理**,`Bot` 会将协议端上报的数据转化为 `Event`(事件),之后 NoneBot 会根据一套既定流程来处理事件。
|
||||
|
||||
2. **调用 `API`**,在**事件处理**的过程中,NoneBot 可以通过 `Bot` 调用协议端指定的 `API` 来获取更多数据,或者反馈响应给协议端;NoneBot 也可以通过调用 `API` 向协议端主动请求数据或者主动推送数据。
|
||||
|
||||
在**指南**模块,我们已经叙述了[如何配置 NoneBot](../tutorial/configuration.md)、[如何注册协议适配器](../tutorial/register-adapter.md)以及[如何加载插件](../tutorial/plugin/load-plugin.md),这里便不再赘述。
|
||||
|
||||
下面,我们将对**事件处理**,**调用 API** 进行说明。
|
||||
|
||||
## 事件处理
|
||||
|
||||
我们可以先看事件处理的流程图:
|
||||
|
||||

|
||||
|
||||
在流程图里,我们可以看到,NoneBot 会有三个阶段来处理事件:
|
||||
|
||||
1. **Driver 接收上报数据**
|
||||
2. **Adapter 处理原始数据**
|
||||
3. **NoneBot 处理 Event**
|
||||
|
||||
我们将顺序说明这三个阶段。其中,会将第三个阶段拆分成**概念解释**,**处理 Event**,**特殊异常处理**三个部分来说明。
|
||||
|
||||
### Driver 接收上报数据
|
||||
|
||||
1. 协议端会通过 websocket 或 http 等方式与 NoneBot 的后端驱动 `Driver` 连接,协议端上报数据后,`Driver` 会将原始数据交给 `Adapter` 处理。
|
||||
|
||||
:::warning
|
||||
连接之前必须要注册 `Adapter`
|
||||
:::
|
||||
|
||||
### Adapter 处理原始数据
|
||||
|
||||
1. `Adapter` 检查授权许可,并获取 `self-id` 作为唯一识别 id 。
|
||||
|
||||
:::tip
|
||||
如果协议端通过 websocket 上报数据,这个步骤只会在建立连接时进行,并在之后运行 `on_bot_connect` 钩子函数;通过 http 方式连接时,会在协议端每次上报数据时都进行这个步骤。
|
||||
:::
|
||||
|
||||
:::warning
|
||||
`self-id` 是帐号的唯一识别 ID ,这意味着不能出现相同的 `self-id`。
|
||||
:::
|
||||
|
||||
2. 根据 `self-id` 实例化 `Adapter` 相应的 `Bot` 。
|
||||
|
||||
3. 根据 `Event Model` 将原始数据转化为 NoneBot 可以处理的 `Event` 对象。
|
||||
|
||||
:::tip
|
||||
`Adapter` 在转换数据格式的同时可以进行一系列的特殊操作,例如 OneBot 适配器会对 reply 信息进行提取。
|
||||
:::
|
||||
|
||||
4. `Bot` 和 `Event` 交由 NoneBot 进一步处理。
|
||||
|
||||
### NoneBot 处理 Event
|
||||
|
||||
在讲述这个阶段之前,我们需要先对几个概念进行解释。
|
||||
|
||||
#### 概念解释
|
||||
|
||||
1. **hook** ,或者说**钩子函数**,它们可以在 NoneBot 处理 `Event` 的不同时刻进行拦截,修改或者扩展,在 NoneBot 中,事件钩子函数分为`事件预处理 hook`、`运行预处理 hook`、`运行后处理 hook` 和`事件后处理 hook`。
|
||||
|
||||
:::tip
|
||||
关于 `hook` 的更多信息,可以查阅[这里](./runtime-hook.md)。
|
||||
:::
|
||||
|
||||
2. **Matcher** 与 **matcher**,在**指南**中,我们讲述了[如何注册事件响应器](../tutorial/plugin/create-matcher.md),这里的事件响应器或者说 `Matcher` 并不是一个具体的实例 `instance`,而是一个具有特定属性的类 `class`。只有当 `Matcher` **响应事件**时,才会实例化为具体的 `instance`,也就是 `matcher` 。`matcher` 可以认为是 NoneBot 处理 `Event` 的基本单位,运行 `matcher` 是 NoneBot 工作的主要内容。
|
||||
|
||||
3. **handler**,或者说**事件处理函数**,它们可以认为是 NoneBot 处理 `Event` 的最小单位。在不考虑 `hook` 的情况下,**运行 matcher 就是顺序运行 matcher.handlers**,这句话换种表达方式就是,`handler` 只有添加到 `matcher.handlers` 时,才可以参与到 NoneBot 的工作中来。
|
||||
|
||||
:::tip
|
||||
如何让 `handler` 添加到 `matcher.handlers`?
|
||||
|
||||
一方面,我们可以参照[这里](../tutorial/plugin/create-handler.md)利用装饰器来添加;另一方面,我们在用 `on()` 或者 `on_*()` 注册事件响应器时,可以添加 `handlers=[handler1, handler2, ...]` 这样的关键词参数来添加。
|
||||
:::
|
||||
|
||||
#### 处理 Event
|
||||
|
||||
1. **执行事件预处理 hook**, NoneBot 接收到 `Event` 后,会传入到 `事件预处理 hook` 中进行处理。
|
||||
|
||||
:::warning
|
||||
需要注意的是,执行多个 `事件预处理 hook` 时并无顺序可言,它们是**并发运行**的。这个原则同样适用于其他的 `hook`。
|
||||
:::
|
||||
|
||||
2. **按优先级升序选出同一优先级的 Matcher**,NoneBot 提供了一个全局字典 `matchers`,这个字典的 `key` 是优先级 `priority`,`value` 是一个 `list`,里面存放着同一优先级的 `Matcher`。在注册 `Matcher` 时,它和优先级 `priority` 会添加到里面。
|
||||
|
||||
在执行 `事件预处理 hook` 后,NoneBot 会对 `matchers` 的 `key` 升序排序并选择出当前最小优先级的 `Matcher`。
|
||||
|
||||
3. **根据 Matcher 定义的 Rule、Permission 判断是否运行**,在选出 `Matcher` 后,NoneBot 会将 `bot`,`Event` 传入到 `Matcher.check_rule` 和 `Matcher.check_perm` 两个函数中,两个函数分别对 Matcher 定义的 `Rule`、`Permission` 进行 check,当 check 通过后,这个 `Matcher` 就会响应事件。当同一个优先级的所有 `Matcher` 均没有响应时,NoneBot 会返回到上一个步骤,选择出下一优先级的 `Matcher`。
|
||||
|
||||
4. **实例化 matcher 并执行运行预处理 hook**,当 `Matcher` 响应事件后,它便会实例化为 `matcher`,并执行 `运行预处理 hook`。
|
||||
|
||||
5. **顺序运行 matcher 的所有 handlers**,`运行预处理 hook` 执行完毕后,便会运行 `matcher`,也就是**顺序运行**它的 `handlers`。
|
||||
|
||||
:::tip
|
||||
`matcher` 运行 `handlers` 的顺序是:先运行该 `matcher` 的类 `Matcher` 注册时添加的 `handlers`(如果有的话),再按照装饰器装饰顺序运行装饰的 `handlers`。
|
||||
:::
|
||||
|
||||
6. **执行运行后处理 hook**,`matcher` 的 `handlers` 运行完毕后,会执行 `运行后处理 hook`。
|
||||
|
||||
7. **判断是否停止事件传播**,NoneBot 会根据当前优先级所有 `matcher` 的 `block` 参数或者 `StopPropagation` 异常判断是否停止传播 `Event`,如果事件没有停止传播,NoneBot 便会返回到第 2 步, 选择出下一优先级的 `Matcher`。
|
||||
|
||||
8. **执行事件后处理 hook**,在 `Event` 停止传播或执行完所有响应的 `Matcher` 后,NoneBot 会执行 `事件后处理 hook`。
|
||||
|
||||
当 `事件后处理 hook` 执行完毕后,当前 `Event` 的处理周期就顺利结束了。
|
||||
|
||||
#### 特殊异常处理
|
||||
|
||||
在这个阶段,NoneBot 规定了几个特殊的异常,当 NoneBot 捕获到它们时,会用特定的行为来处理它们。
|
||||
|
||||
1. **IgnoredException**
|
||||
|
||||
这个异常可以在 `事件预处理 hook` 和 `运行预处理 hook` 抛出。
|
||||
|
||||
当 `事件预处理 hook` 抛出它时,NoneBot 会忽略当前的 `Event`,不进行处理。
|
||||
|
||||
当 `运行预处理 hook` 抛出它时,NoneBot 会忽略当前的 `matcher`,结束当前 `matcher` 的运行。
|
||||
|
||||
:::warning
|
||||
当 `hook` 需要抛出这个异常时,要写明原因。
|
||||
:::
|
||||
|
||||
2. **PausedException**
|
||||
|
||||
这个异常可以在 `handler` 中由 `Matcher.pause` 抛出。
|
||||
|
||||
当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将后续的 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行后续的 `handler`。
|
||||
|
||||
3. **RejectedException**
|
||||
|
||||
这个异常可以在 `handler` 中由 `Matcher.reject` 抛出。
|
||||
|
||||
当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将当前 handler 和后续 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行当前 `handler` 和后续的 `handler` 。
|
||||
|
||||
4. **FinishedException**
|
||||
|
||||
这个异常可以在 `handler` 中由 `Matcher.finish` 抛出。
|
||||
|
||||
当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行。
|
||||
|
||||
5. **StopPropagation**
|
||||
|
||||
这个异常一般会在执行 `运行后处理 hook` 后抛出。
|
||||
|
||||
当 NoneBot 捕获到它时, 会停止传播当前 `Event` ,不再寻找下一优先级的 `Matcher` ,直接执行 `事件后处理 hook` 。
|
||||
|
||||
## 调用 API
|
||||
|
||||
NoneBot 可以通过 `bot` 来调用 `API`,`API` 可以向协议端发送数据,也可以向协议端请求更多的数据。
|
||||
|
||||
NoneBot 调用 `API` 会有如下过程:
|
||||
|
||||
1. 调用 `calling_api_hook` 预处理钩子。
|
||||
|
||||
2. `adapter` 将信息处理为原始数据,并转交 `driver`,`driver` 交给协议端处理。
|
||||
|
||||
3. `driver` 接收协议端的结果,交给`adapter` 处理之后将结果反馈给 NoneBot 。
|
||||
|
||||
4. 调用 `called_api_hook` 后处理钩子。
|
||||
|
||||
在调用 `API` 时同样规定了特殊的异常,叫做 `MockApiException` 。该异常会由预处理钩子和后处理钩子触发,当预处理钩子触发时,NoneBot 会跳过之后的调用过程,直接执行后处理钩子。
|
||||
|
||||
:::tip
|
||||
不同 `adapter` 规定了不同的 API,对应的 API 列表请参照协议规范。
|
||||
:::
|
||||
|
||||
一般来说,我们可以用 `bot.*` 来调用 `API`(\*是 `API` 的 `action` 或者 `endpoint`)。
|
||||
|
||||
对于发送消息而言,一方面可以调用既有的 `API` ;另一方面 NoneBot 实现了两个便捷方法,`bot.send(event, message, **kwargs)` 方法和可以在 `handler` 中使用的 `Matcher.send(message, **kwargs)` 方法,来向事件主体发送消息。
|
161
website/docs/advanced/adapter.md
Normal file
161
website/docs/advanced/adapter.md
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 注册适配器与指定平台交互
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 使用适配器
|
||||
|
||||
适配器 (Adapter) 是机器人与平台交互的核心桥梁,它负责在驱动器和机器人插件之间转换与传递消息。
|
||||
|
||||
## 适配器功能与组成
|
||||
|
||||
适配器通常有两种功能,分别是**接收事件**和**调用平台接口**。其中,接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型,然后交由机器人插件处理;调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式,然后交由驱动器发送,并接收接口返回数据。
|
||||
|
||||
为了实现这两种功能,适配器通常由四个部分组成:
|
||||
|
||||
- **Adapter**:负责转换事件和调用接口,正确创建 Bot 对象并注册到 NoneBot 中。
|
||||
- **Bot**:负责存储平台机器人相关信息,并提供回复事件的方法。
|
||||
- **Event**:负责定义事件内容,以及事件主体对象。
|
||||
- **Message**:负责正确序列化消息,以便机器人插件处理。
|
||||
|
||||
## 注册适配器
|
||||
|
||||
在使用适配器之前,我们需要先将适配器注册到驱动器中,这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例,来看看如何注册适配器:
|
||||
|
||||
```python {2,5} title=bot.py
|
||||
import nonebot
|
||||
from nonebot.adapters.console import Adapter
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
driver.register_adapter(Adapter)
|
||||
```
|
||||
|
||||
我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。
|
||||
|
||||
## 获取已注册的适配器
|
||||
|
||||
NoneBot 提供了 `get_adapter` 方法来获取已注册的适配器,我们可以通过适配器的名称或类型来获取指定的适配器实例:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
from nonebot.adapters.console import Adapter
|
||||
|
||||
adapters = nonebot.get_adapters()
|
||||
console_adapter = nonebot.get_adapter(Adapter)
|
||||
console_adapter = nonebot.get_adapter(Adapter.get_name())
|
||||
```
|
||||
|
||||
## 获取 Bot 对象
|
||||
|
||||
当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取,这是一个以机器人 ID 为键的字典:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
bots = nonebot.get_bots()
|
||||
```
|
||||
|
||||
我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数,将会返回所有 Bot 中的第一个:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
bot = nonebot.get_bot("bot_id")
|
||||
```
|
||||
|
||||
如果需要获取指定适配器连接的 Bot 对象,我们可以通过适配器的 `bots` 属性获取,这也是一个以机器人 ID 为键的字典:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
from nonebot.adapters.console import Adapter
|
||||
|
||||
console_adapter = nonebot.get_adapter(Adapter)
|
||||
bots = console_adapter.bots
|
||||
```
|
||||
|
||||
Bot 对象都具有一个 `self_id` 属性,它是机器人的唯一 ID,由适配器填写,通常为机器人的帐号 ID 或者 APP ID。
|
||||
|
||||
## 获取事件通用信息
|
||||
|
||||
适配器的所有事件模型均继承自 `Event` 基类,在[事件类型与重载](../appendices/overload.md)一节中,我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息:
|
||||
|
||||
### 事件类型
|
||||
|
||||
事件类型通常为 `meta_event`、`message`、`notice`、`request`。
|
||||
|
||||
```python
|
||||
type: str = event.get_type()
|
||||
```
|
||||
|
||||
### 事件名称
|
||||
|
||||
事件名称由适配器定义,通常用于日志记录。
|
||||
|
||||
```python
|
||||
name: str = event.get_event_name()
|
||||
```
|
||||
|
||||
### 事件描述
|
||||
|
||||
事件描述由适配器定义,通常用于日志记录。
|
||||
|
||||
```python
|
||||
description: str = event.get_event_description()
|
||||
```
|
||||
|
||||
### 事件日志字符串
|
||||
|
||||
事件日志字符串由事件名称和事件描述组成,用于日志记录。
|
||||
|
||||
```python
|
||||
log: str = event.get_log_string()
|
||||
```
|
||||
|
||||
### 事件主体 ID
|
||||
|
||||
事件主体 ID 通常为机器人用户 ID。
|
||||
|
||||
```python
|
||||
user_id: str = event.get_user_id()
|
||||
```
|
||||
|
||||
### 事件会话 ID
|
||||
|
||||
事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。
|
||||
|
||||
```python
|
||||
session_id: str = event.get_session_id()
|
||||
```
|
||||
|
||||
### 事件消息
|
||||
|
||||
如果事件包含消息,则可以通过该方法获取,否则会产生异常。
|
||||
|
||||
```python
|
||||
message: Message = event.get_message()
|
||||
```
|
||||
|
||||
### 事件纯文本消息
|
||||
|
||||
通常为事件消息的纯文本内容,如果事件不包含消息,则会产生异常。
|
||||
|
||||
```python
|
||||
text: str = event.get_plaintext()
|
||||
```
|
||||
|
||||
### 事件是否与机器人有关
|
||||
|
||||
由适配器实现的判断,通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。
|
||||
|
||||
```python
|
||||
is_tome: bool = event.is_tome()
|
||||
```
|
||||
|
||||
## 更多
|
||||
|
||||
官方支持的适配器和社区贡献的适配器均可在[商店](/store)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。
|
1189
website/docs/advanced/dependency.mdx
Normal file
1189
website/docs/advanced/dependency.mdx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"label": "依赖注入",
|
||||
"position": 5
|
||||
}
|
@@ -1,243 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 依赖注入简介
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 60
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 简介
|
||||
|
||||
受 [FastAPI](https://fastapi.tiangolo.com/tutorial/dependencies/) 启发,NoneBot 同样编写了一个简易的依赖注入模块,使得开发者可以通过事件处理函数参数的类型标注来自动注入依赖。
|
||||
|
||||
## 什么是依赖注入?
|
||||
|
||||
[依赖注入](https://zh.wikipedia.org/wiki/%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5)
|
||||
|
||||
> 在软件工程中,**依赖注入**(dependency injection)的意思为,给予调用方它所需要的事物。 “依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖。 传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。
|
||||
|
||||
依赖注入往往起到了分离依赖和调用方的作用,这样一方面能让代码更为整洁可读,一方面可以提升代码的复用性。
|
||||
|
||||
## 使用依赖注入
|
||||
|
||||
以下通过一个简单的例子来说明依赖注入的使用方法:
|
||||
|
||||
```python {2,7-8,11}
|
||||
from nonebot import on_command
|
||||
from nonebot.params import Depends # 1.引用 Depends
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
async def depend(event: MessageEvent): # 2.编写依赖函数
|
||||
return {"uid": event.get_user_id(), "nickname": event.sender.nickname}
|
||||
|
||||
@test.handle()
|
||||
async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项
|
||||
print(x["uid"], x["nickname"])
|
||||
```
|
||||
|
||||
如注释所言,可以用三步来说明依赖注入的使用过程:
|
||||
|
||||
1. 引用 `Depends` 。
|
||||
|
||||
2. 编写依赖函数。依赖函数和普通的事件处理函数并无区别,同样可以接收 `bot`, `event`, `state` 等参数,你可以把它当作一个普通的事件处理函数,但是去除了装饰器(没有使用 `matcher.handle()` 等来装饰),并且可以返回任何类型的值。
|
||||
|
||||
在这里我们接受了 `event`,并以 `onebot` 的 `MessageEvent` 作为类型标注,返回一个新的字典,包括 `uid` 和 `nickname` 两个键值。
|
||||
|
||||
3. 在事件处理函数中声明依赖项。依赖项必须要 `Depends` 包裹依赖函数作为默认值。
|
||||
|
||||
:::tip
|
||||
请注意,参数 `x` 的类型标注将会影响到事件处理函数的运行,与类型标注不符的值将会导致事件处理函数被跳过。
|
||||
:::
|
||||
|
||||
:::tip
|
||||
事实上,bot、event、state 它们本身只是依赖注入的一个特例,它们无需声明这是依赖即可注入。
|
||||
:::
|
||||
|
||||
虽然声明依赖项的方式和其他参数如 `bot`, `event` 并无二样,但他的参数有一些限制,必须是**可调用对象**,函数自然是可调用对象,类和生成器也是,我们会在接下来的小节说明。
|
||||
|
||||
一般来说,当接收到事件时,`NoneBot2` 会进行以下处理:
|
||||
|
||||
1. 准备依赖函数所需要的参数。
|
||||
2. 调用依赖函数并获得返回值。
|
||||
3. 将返回值作为事件处理函数中的参数值传入。
|
||||
|
||||
## 依赖缓存
|
||||
|
||||
在使用 `Depends` 包裹依赖函数时,有一个参数 `use_cache` ,它默认为 `True` ,这个参数会决定 `Nonebot2` 在依赖注入的处理中是否使用缓存。
|
||||
|
||||
```python {11}
|
||||
import random
|
||||
from nonebot import on_command
|
||||
from nonebot.params import Depends
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
async def always_run():
|
||||
return random.randint(1, 100)
|
||||
|
||||
@test.handle()
|
||||
async def _(x: int = Depends(always_run, use_cache=False)):
|
||||
print(x)
|
||||
```
|
||||
|
||||
:::tip
|
||||
缓存是针对单次事件处理来说的,在事件处理中 `Depends` 第一次被调用时,结果存入缓存,在之后都会直接返回缓存中的值,在事件处理结束后缓存就会被清除。
|
||||
:::
|
||||
|
||||
当使用缓存时,依赖注入会这样处理:
|
||||
|
||||
1. 查询缓存,如果缓存中有相应的值,则直接返回。
|
||||
2. 准备依赖函数所需要的参数。
|
||||
3. 调用依赖函数并获得返回值。
|
||||
4. 将返回值存入缓存。
|
||||
5. 将返回值作为事件处理函数中的参数值传入。
|
||||
|
||||
## 同步支持
|
||||
|
||||
我们在编写依赖函数时,可以简单地用同步函数,`NoneBot2` 的内部流程会进行处理:
|
||||
|
||||
```python {2,8-9,12}
|
||||
from nonebot.log import logger
|
||||
from nonebot.params import Depends # 1.引用 Depends
|
||||
from nonebot import on_command, on_message
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
def depend(event: MessageEvent): # 2.编写同步依赖函数
|
||||
return {"uid": event.get_user_id(), "nickname": event.sender.nickname}
|
||||
|
||||
@test.handle()
|
||||
async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项
|
||||
print(x["uid"], x["nickname"])
|
||||
```
|
||||
|
||||
## Class 作为依赖
|
||||
|
||||
我们可以看下面的代码段:
|
||||
|
||||
```python
|
||||
class A:
|
||||
def __init__(self):
|
||||
pass
|
||||
a = A()
|
||||
```
|
||||
|
||||
在我们实例化类 `A` 的时候,其实我们就在**调用**它,类本身也是一个**可调用对象**,所以类可以被 `Depends` 包裹成为依赖项。
|
||||
|
||||
因此我们对第一节的代码段做一下改造:
|
||||
|
||||
```python {2,7-10,13}
|
||||
from nonebot import on_command
|
||||
from nonebot.params import Depends # 1.引用 Depends
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
class DependClass: # 2.编写依赖类
|
||||
def __init__(self, event: MessageEvent):
|
||||
self.uid = event.get_user_id()
|
||||
self.nickname = event.sender.nickname
|
||||
|
||||
@test.handle()
|
||||
async def _(x: DependClass = Depends(DependClass)): # 3.在事件处理函数里声明依赖项
|
||||
print(x.uid, x.nickname)
|
||||
```
|
||||
|
||||
依然可以用三步说明如何用类作为依赖项:
|
||||
|
||||
1. 引用 `Depends` 。
|
||||
2. 编写依赖类。类的 `__init__` 函数可以接收 `bot`, `event`, `state` 等参数,在这里我们接受了 `event`,并以 `onebot` 的 `MessageEvent` 作为类型标注。
|
||||
3. 在事件处理函数中声明依赖项。当用类作为依赖项时,它会是一个对应的实例,在这里 `x` 就是 `DependClass` 实例。
|
||||
|
||||
### 另一种依赖项声明方式
|
||||
|
||||
当使用类作为依赖项时,`Depends` 的参数可以为空,`NoneBot2` 会根据参数的类型标注进行推断并进行依赖注入。
|
||||
|
||||
```python
|
||||
@test.handle()
|
||||
async def _(x: DependClass = Depends()): # 在事件处理函数里声明依赖项
|
||||
print(x.uid, x.nickname)
|
||||
```
|
||||
|
||||
## 生成器作为依赖
|
||||
|
||||
:::warning
|
||||
`yield` 语句只能写一次,否则会引发异常。
|
||||
如果对此有疑问并想探究原因,可以看 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档,实际上,`Nonebot2` 的内部就使用了这两个装饰器。
|
||||
:::
|
||||
|
||||
:::tips
|
||||
生成器是 `Python` 高级特性,如果你对此处文档感到疑惑那说明暂时你还用不上这个功能。
|
||||
:::
|
||||
|
||||
与 `FastAPI` 一样,`NoneBot2` 的依赖注入支持依赖项在事件处理结束后进行一些额外的工作,比如数据库 session 或者网络 IO 的关闭,互斥锁的解锁等等。
|
||||
|
||||
要实现上述功能,我们可以用生成器函数作为依赖项,我们用 `yield` 关键字取代 `return` 关键字,并在 `yield` 之后进行额外的工作。
|
||||
|
||||
我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO:
|
||||
|
||||
```python {3,7-10,13}
|
||||
import httpx
|
||||
from nonebot import on_command
|
||||
from nonebot.params import Depends # 1.引用 Depends
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
async def get_client(): # 2.编写异步生成器函数
|
||||
async with httpx.AsyncClient() as client:
|
||||
yield client
|
||||
print("调用结束")
|
||||
|
||||
@test.handle()
|
||||
async def _(x: httpx.AsyncClient = Depends(get_client)): # 3.在事件处理函数里声明依赖项
|
||||
resp = await x.get("https://v2.nonebot.dev")
|
||||
# do something
|
||||
```
|
||||
|
||||
我们用 `yield` 代码段作为生成器函数的“返回”,在事件处理函数里用返回出来的 `client` 做自己需要的工作。在 `NoneBot2` 结束事件处理时,会执行 `yield` 之后的代码。
|
||||
|
||||
## 创造可调用对象作为依赖
|
||||
|
||||
:::tips
|
||||
魔法方法 `__call__` 是 `Python` 高级特性,如果你对此处文档感到疑惑那说明暂时你还用不上这个功能。
|
||||
:::
|
||||
|
||||
在 `Python` 的里,类的 `__call__` 方法会让类的实例变成**可调用对象**,我们可以利用这个魔法方法做一个简单的尝试:
|
||||
|
||||
```python{3,9-14,16,19}
|
||||
from typing import Type
|
||||
from nonebot.log import logger
|
||||
from nonebot.params import Depends # 1.引用 Depends
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent
|
||||
|
||||
test = on_command("123")
|
||||
|
||||
class EventChecker: # 2.编写需要的类
|
||||
def __init__(self, EventClass: Type[MessageEvent]):
|
||||
self.event_class = EventClass
|
||||
|
||||
def __call__(self, event: MessageEvent) -> bool:
|
||||
return isinstance(event, self.event_class)
|
||||
|
||||
checker = EventChecker(GroupMessageEvent) # 3.将类实例化
|
||||
|
||||
@test.handle()
|
||||
async def _(x: bool = Depends(checker)): # 4.在事件处理函数里声明依赖项
|
||||
if x:
|
||||
print("这是群聊消息")
|
||||
else:
|
||||
print("这不是群聊消息")
|
||||
```
|
||||
|
||||
这是判断 `onebot` 的消息事件是不是群聊消息事件的一个例子,我们可以用四步来说明这个例子:
|
||||
|
||||
1. 引用 `Depends` 。
|
||||
2. 编写需要的类。类的 `__init__` 函数接收参数 `EventClass`,它将接收事件类本身。类的 `__call__` 函数将接受消息事件对象,并返回一个 `bool` 类型的判定结果。
|
||||
3. 将类实例化。我们传入群聊消息事件作为参数实例化 `checker` 。
|
||||
4. 在事件处理函数里声明依赖项。`NoneBot2` 将会调用 `checker` 的 `__call__` 方法,返回给参数 `x` 相应的判断结果。
|
@@ -1,76 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: 重载事件处理函数
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 61
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 事件处理函数重载
|
||||
|
||||
当我们在编写 NoneBot2 应用时,常常会遇到这样一个问题:该怎么让同一类型的不同事件执行不同的响应逻辑?又或者如何让不同的 `bot` 针对同一类型的事件作出不同响应?
|
||||
|
||||
针对这个问题, NoneBot2 提供一个便捷而高效的解决方案:事件处理函数重载机制。简单地说,`handler`(事件处理函数)会根据其参数的 `type hints`([PEP484 类型标注](https://www.python.org/dev/peps/pep-0484/))来对相对应的 `bot` 和 `event` 进行响应,并且会忽略不符合其参数类型标注的情况。
|
||||
|
||||
<!-- 必须要注意的是,该机制利用了 `inspect` 标准库获取到了事件处理函数的 `signature`(签名),进一步获取到参数名称和类型标注。故而,我们在编写 `handler` 时,参数的名称和类型标注必须要符合 `T_Handler` 规定,详情可以参看**指南**中的[事件处理](../../guide/creating-a-handler)。 -->
|
||||
|
||||
:::tip 提示
|
||||
如果想了解更多关于 `inspect` 标准库的信息,可以查看[官方文档](https://docs.python.org/zh-cn/3.9/library/inspect.html)。
|
||||
:::
|
||||
|
||||
下面,我们会以 OneBot 适配器中的群聊消息事件和私聊消息事件为例,对该机制的应用进行简单的介绍。
|
||||
|
||||
## 一个例子
|
||||
|
||||
首先,我们需要导入需要的方法、类型。
|
||||
|
||||
```python
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, PrivateMessageEvent
|
||||
```
|
||||
|
||||
之后,我们可以注册一个 `Matcher` 来响应消息事件。
|
||||
|
||||
```python
|
||||
matcher = on_command("test_overload")
|
||||
```
|
||||
|
||||
最后,我们编写不同的 `handler` 并编写不同的类型标注来实现事件处理函数重载:
|
||||
|
||||
```python
|
||||
@matcher.handle()
|
||||
async def _(bot: Bot, event: GroupMessageEvent):
|
||||
await matcher.send("群聊消息事件响应成功!")
|
||||
|
||||
|
||||
@matcher.handle()
|
||||
async def _(bot: Bot, event: PrivateMessageEvent):
|
||||
await matcher.send("私聊消息事件响应成功!")
|
||||
```
|
||||
|
||||
此时,我们可以在群聊或私聊中对我们的机器人发送 `test_overload`,它会在不同的场景做出不同的应答。
|
||||
|
||||
这样一个简单的事件处理函数重载就完成了。
|
||||
|
||||
## 进阶
|
||||
|
||||
事件处理函数重载机制同样支持被 `matcher.got` 等装饰器装饰的函数。例如:
|
||||
|
||||
```python
|
||||
@matcher.got("key1", prompt="群事件提问")
|
||||
async def _(bot: Bot, event: GroupMessageEvent):
|
||||
await matcher.send("群聊消息事件响应成功!")
|
||||
|
||||
|
||||
@matcher.got("key2", prompt="私聊事件提问")
|
||||
async def _(bot: Bot, event: PrivateMessageEvent):
|
||||
await matcher.send("私聊消息事件响应成功!")
|
||||
```
|
||||
|
||||
只有触发事件符合的函数才会触发装饰器。
|
||||
|
||||
:::warning 注意
|
||||
bot 和 event 参数具有最高的检查优先级,因此,如果参数类型不符合,所有的依赖项 `Depends` 等都不会被执行。
|
||||
:::
|
286
website/docs/advanced/driver.md
Normal file
286
website/docs/advanced/driver.md
Normal file
@@ -0,0 +1,286 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
description: 选择合适的驱动器运行机器人
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 10
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 选择驱动器
|
||||
|
||||
驱动器 (Driver) 是机器人运行的基石,它是机器人初始化的第一步,主要负责数据收发。
|
||||
|
||||
:::important 提示
|
||||
驱动器的选择通常与机器人所使用的协议适配器相关,如果不知道该选择哪个驱动器,可以先阅读相关协议适配器文档说明。
|
||||
:::
|
||||
|
||||
:::tip 提示
|
||||
如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。
|
||||
:::
|
||||
|
||||
## 驱动器类型
|
||||
|
||||
驱动器的类型有两种:
|
||||
|
||||
- `ForwardDriver`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。
|
||||
- `ReverseDriver`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。
|
||||
|
||||
客户端型驱动器具有以下两种功能:
|
||||
|
||||
1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。
|
||||
2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。
|
||||
|
||||
服务端型驱动器通常为 ASGI 应用框架,具有以下功能:
|
||||
|
||||
1. 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。
|
||||
2. 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。
|
||||
3. 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。
|
||||
|
||||
## 配置驱动器
|
||||
|
||||
驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍,这里将详细介绍驱动器配置的格式。
|
||||
|
||||
NoneBot 中的客户端和服务端型驱动器可以相互配合使用,但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类,即驱动器类,他可以作为驱动器单独运行。同时,客户端驱动器模块中还会提供一个 `Mixin` 子类,用于在与其他驱动器配合使用时加载。因此,驱动器配置格式采用特殊语法:`<module>[:<Driver>][+<module>[:<Mixin>]]*`。
|
||||
|
||||
其中,`<module>` 代表**驱动器模块路径**;`<Driver>` 代表**驱动器类名**,默认为 `Driver`;`<Mixin>` 代表**驱动器混入类名**,默认为 `Mixin`。即,我们需要选择一个主要驱动器,然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型,但混入类驱动器只能为客户端类型。
|
||||
|
||||
特别的,为了简化内置驱动器模块路径,我们可以使用 `~` 符号作为内置驱动器模块路径的前缀,如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配,但需要安装额外依赖才能使用,具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下:
|
||||
|
||||
```dotenv
|
||||
DRIVER=~fastapi
|
||||
DRIVER=~aiohttp
|
||||
DRIVER=~httpx+~websockets
|
||||
DRIVER=~fastapi+~httpx+~websockets
|
||||
```
|
||||
|
||||
## 获取驱动器
|
||||
|
||||
在 NoneBot 框架初始化完成后,我们就可以通过 `get_driver()` 方法获取全局驱动器实例:
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
driver = get_driver()
|
||||
```
|
||||
|
||||
## 内置驱动器
|
||||
|
||||
### None
|
||||
|
||||
**类型:**服务端驱动器
|
||||
|
||||
NoneBot 内置的空驱动器,不提供任何收发数据功能,可以在不需要外部网络连接时使用。
|
||||
|
||||
```env
|
||||
DRIVER=~none
|
||||
```
|
||||
|
||||
### FastAPI(默认)
|
||||
|
||||
**类型:**服务端驱动器
|
||||
|
||||
> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
|
||||
|
||||
[FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架,具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能,也可以挂载其他 ASGI、WSGI 应用。
|
||||
|
||||
```env
|
||||
DRIVER=~fastapi
|
||||
```
|
||||
|
||||
#### FastAPI 配置项
|
||||
|
||||
##### `fastapi_openapi_url`
|
||||
|
||||
类型:`str | None`
|
||||
默认值:`None`
|
||||
说明:`FastAPI` 提供的 `OpenAPI` JSON 定义地址,如果为 `None`,则不提供 `OpenAPI` JSON 定义。
|
||||
|
||||
##### `fastapi_docs_url`
|
||||
|
||||
类型:`str | None`
|
||||
默认值:`None`
|
||||
说明:`FastAPI` 提供的 `Swagger` 文档地址,如果为 `None`,则不提供 `Swagger` 文档。
|
||||
|
||||
##### `fastapi_redoc_url`
|
||||
|
||||
类型:`str | None`
|
||||
默认值:`None`
|
||||
说明:`FastAPI` 提供的 `ReDoc` 文档地址,如果为 `None`,则不提供 `ReDoc` 文档。
|
||||
|
||||
##### `fastapi_include_adapter_schema`
|
||||
|
||||
类型:`bool`
|
||||
默认值:`True`
|
||||
说明:`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。
|
||||
|
||||
##### `fastapi_reload`
|
||||
|
||||
:::warning 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
nb run --reload
|
||||
```
|
||||
|
||||
开启该功能后,在 uvicorn 运行时(FastAPI 提供的 ASGI 底层,即 reload 功能的实际来源),asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。
|
||||
|
||||
> 相关信息参考 [uvicorn#529](https://github.com/encode/uvicorn/issues/529),[uvicorn#1070](https://github.com/encode/uvicorn/pull/1070),[uvicorn#1257](https://github.com/encode/uvicorn/pull/1257)
|
||||
|
||||
后者(`SelectorEventLoop`)在 Windows 平台的可使用性不如前者(`ProactorEventLoop`),包括但不限于
|
||||
|
||||
1. 不支持创建子进程
|
||||
2. 最多只支持 512 个套接字
|
||||
3. ...
|
||||
|
||||
> 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows)
|
||||
|
||||
所以,一些使用了 asyncio 的库因此可能无法正常工作,如:
|
||||
|
||||
1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows)
|
||||
|
||||
如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`),
|
||||
你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能。
|
||||
:::
|
||||
|
||||
类型:`bool`
|
||||
默认值:`False`
|
||||
说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。
|
||||
|
||||
```python title=bot.py
|
||||
app = nonebot.get_asgi()
|
||||
nonebot.run(app="bot:app")
|
||||
```
|
||||
|
||||
##### `fastapi_reload_dirs`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:重载监控文件夹列表,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_reload_delay`
|
||||
|
||||
类型:`float | None`
|
||||
默认值:`None`
|
||||
说明:重载延迟,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_reload_includes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_reload_excludes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `fastapi_extra`
|
||||
|
||||
类型:`Dist[str, Any]`
|
||||
默认值:`{}`
|
||||
说明:传递给 `FastAPI` 的其他参数
|
||||
|
||||
### Quart
|
||||
|
||||
**类型:**`ReverseDriver`
|
||||
|
||||
> Quart is an asyncio reimplementation of the popular Flask microframework API.
|
||||
|
||||
[Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本,拥有与 Flask 非常相似的接口和使用方法。
|
||||
|
||||
```env
|
||||
DRIVER=~quart
|
||||
```
|
||||
|
||||
#### Quart 配置项
|
||||
|
||||
##### `quart_reload`
|
||||
|
||||
:::warning 警告
|
||||
不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。
|
||||
|
||||
```bash
|
||||
nb run --reload
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
类型:`bool`
|
||||
默认值:`False`
|
||||
说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。
|
||||
|
||||
```python title=bot.py
|
||||
app = nonebot.get_asgi()
|
||||
nonebot.run(app="bot:app")
|
||||
```
|
||||
|
||||
##### `quart_reload_dirs`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:重载监控文件夹列表,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_reload_delay`
|
||||
|
||||
类型:`float | None`
|
||||
默认值:`None`
|
||||
说明:重载延迟,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_reload_includes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_reload_excludes`
|
||||
|
||||
类型:`List[str] | None`
|
||||
默认值:`None`
|
||||
说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值
|
||||
|
||||
##### `quart_extra`
|
||||
|
||||
类型:`Dist[str, Any]`
|
||||
默认值:`{}`
|
||||
说明:传递给 `Quart` 的其他参数
|
||||
|
||||
### HTTPX
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
|
||||
:::warning 注意
|
||||
本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。
|
||||
:::
|
||||
|
||||
> [HTTPX](https://www.python-httpx.org/) is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2.
|
||||
|
||||
```env
|
||||
DRIVER=~httpx
|
||||
```
|
||||
|
||||
### websockets
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
|
||||
:::warning 注意
|
||||
本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。
|
||||
:::
|
||||
|
||||
> [websockets](https://websockets.readthedocs.io/) is a library for building WebSocket servers and clients in Python with a focus on correctness, simplicity, robustness, and performance.
|
||||
|
||||
```env
|
||||
DRIVER=~websockets
|
||||
```
|
||||
|
||||
### AIOHTTP
|
||||
|
||||
**类型:**`ForwardDriver`
|
||||
|
||||
> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python.
|
||||
|
||||
```env
|
||||
DRIVER=~aiohttp
|
||||
```
|
Binary file not shown.
Before Width: | Height: | Size: 376 KiB |
Binary file not shown.
Before Width: | Height: | Size: 103 KiB |
Binary file not shown.
Before Width: | Height: | Size: 128 KiB |
40
website/docs/advanced/matcher-provider.md
Normal file
40
website/docs/advanced/matcher-provider.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
sidebar_position: 10
|
||||
description: 自定义事件响应器存储
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 110
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 事件响应器存储
|
||||
|
||||
事件响应器是 NoneBot 处理事件的核心,它们默认存储在一个字典中。在进入会话状态后,事件响应器将会转为临时响应器,作为最高优先级同样存储于该字典中。因此,事件响应器的存储类似于会话存储,它决定了整个 NoneBot 对事件的处理行为。
|
||||
|
||||
NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但是我们也可以自定义事件响应器存储,将事件响应器存储于其他地方,例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。
|
||||
|
||||
## 编写存储提供者
|
||||
|
||||
事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`,即以优先级为键,以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。
|
||||
|
||||
编写一个自定义的存储提供者,只需要继承并实现 `MatcherProvider` 抽象类:
|
||||
|
||||
```python
|
||||
from nonebot.matcher import MatcherProvider
|
||||
|
||||
class CustomProvider(MatcherProvider):
|
||||
...
|
||||
```
|
||||
|
||||
## 设置存储提供者
|
||||
|
||||
我们可以通过 `matchers.set_provider` 方法设置存储提供者:
|
||||
|
||||
```python {3}
|
||||
from nonebot.matcher import matchers
|
||||
|
||||
matchers.set_provider(CustomProvider)
|
||||
|
||||
assert isinstance(matchers.provider, CustomProvider)
|
||||
```
|
304
website/docs/advanced/matcher.md
Normal file
304
website/docs/advanced/matcher.md
Normal file
@@ -0,0 +1,304 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
description: 事件响应器组成与内置响应规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 60
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 事件响应器进阶
|
||||
|
||||
在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,以及内置的响应规则。
|
||||
|
||||
## 事件响应器组成
|
||||
|
||||
### 事件响应器类型
|
||||
|
||||
事件响应器类型 `type` 即是该响应器所要响应的事件类型,只有在接收到的事件类型与该响应器的类型相同时,才会触发该响应器。如果类型为空字符串 `""`,则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查(权限控制、响应规则)之前进行。
|
||||
|
||||
NoneBot 内置了四种常用事件类型:`meta_event`、`message`、`notice`、`request`,分别对应元事件、消息、通知、请求。通常情况下,协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应,可以自行定义新的类型。
|
||||
|
||||
### 事件触发权限
|
||||
|
||||
事件触发权限 `permission` 是一个 `Permission` 对象,这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查,如果权限检查通过,则执行响应规则检查。
|
||||
|
||||
### 事件响应规则
|
||||
|
||||
事件响应规则 `rule` 是一个 `Rule` 对象,这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配,如果响应规则检查通过,则触发该响应器。
|
||||
|
||||
### 响应优先级
|
||||
|
||||
响应优先级 `priority` 是一个正整数,用于指定响应器的优先级。响应器的优先级越小,越先被触发。如果响应器的优先级相同,则按照响应器的注册顺序进行触发。
|
||||
|
||||
### 阻断
|
||||
|
||||
阻断 `block` 是一个布尔值,用于指定响应器是否阻断事件的传播。如果阻断为 `True`,则在该响应器被触发后,事件将不会再传播给其他下一优先级的响应器。
|
||||
|
||||
NoneBot 内置的事件响应器中,所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递,其他则不会。
|
||||
|
||||
在部分情况中,可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播,该方法需要 handler 在参数中获取 matcher 实例后调用方法。
|
||||
|
||||
### 有效期
|
||||
|
||||
事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值,用于指定响应器是否为临时响应器。如果为 `True`,则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象,用于指定响应器的过期时间。如果 `expire_time` 不为 `None`,则在该时间点后,该响应器会被自动销毁。
|
||||
|
||||
### 默认状态
|
||||
|
||||
事件响应器的默认状态 `default_state` 是一个 `dict` 对象,用于指定响应器的默认状态。在响应器被触发时,响应器将会初始化默认状态然后开始执行事件处理流程。
|
||||
|
||||
## 基本辅助函数
|
||||
|
||||
NoneBot 为四种类型的事件响应器提供了五个基本的辅助函数:
|
||||
|
||||
- `on`:创建任何类型的事件响应器。
|
||||
- `on_metaevent`:创建元事件响应器。
|
||||
- `on_message`:创建消息事件响应器。
|
||||
- `on_request`:创建请求事件响应器。
|
||||
- `on_notice`:创建通知事件响应器。
|
||||
|
||||
除了 `on` 函数具有一个 `type` 参数外,其余参数均相同:
|
||||
|
||||
- `rule`:响应规则,可以是 `Rule` 对象或者 `RuleChecker` 函数。
|
||||
- `permission`:事件触发权限,可以是 `Permission` 对象或者 `PermissionChecker` 函数。
|
||||
- `handlers`:事件处理函数列表。
|
||||
- `temp`:是否为临时响应器。
|
||||
- `expire_time`:响应器的过期时间。
|
||||
- `priority`:响应器的优先级。
|
||||
- `block`:是否阻断事件传播。
|
||||
- `state`:响应器的默认状态。
|
||||
|
||||
在消息类型的事件响应器的基础上,NoneBot 还内置了一些常用的响应规则,并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。
|
||||
|
||||
## 内置响应规则
|
||||
|
||||
### `startswith`
|
||||
|
||||
`startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。
|
||||
|
||||
例如,我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import startswith
|
||||
|
||||
rule = startswith(("!", "/"), ignorecase=False)
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_startswith
|
||||
|
||||
matcher = on_startswith(("!", "/"), ignorecase=False)
|
||||
```
|
||||
|
||||
### `endswith`
|
||||
|
||||
`endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。
|
||||
|
||||
例如,我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import endswith
|
||||
|
||||
rule = endswith((".", "。"), ignorecase=False)
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_endswith
|
||||
|
||||
matcher = on_endswith((".", "。"), ignorecase=False)
|
||||
```
|
||||
|
||||
### `fullmatch`
|
||||
|
||||
`fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串(或一系列字符串)完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。
|
||||
|
||||
例如,我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import fullmatch
|
||||
|
||||
rule = fullmatch(("ping", "pong"), ignorecase=False)
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_fullmatch
|
||||
|
||||
matcher = on_fullmatch(("ping", "pong"), ignorecase=False)
|
||||
```
|
||||
|
||||
### `keyword`
|
||||
|
||||
`keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串(或一系列字符串)。
|
||||
|
||||
例如,我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import keyword
|
||||
|
||||
rule = keyword("hello", "hi")
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_keyword
|
||||
|
||||
matcher = on_keyword("hello", "hi")
|
||||
```
|
||||
|
||||
### `command`
|
||||
|
||||
`command` 是最常用的响应规则,它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。
|
||||
|
||||
例如,当我们配置了 `Command Start` 为 `/`,`Command Separator` 为 `.` 时:
|
||||
|
||||
```python
|
||||
from nonebot.rule import command
|
||||
|
||||
# 匹配 "/help" 或者 "/帮助" 开头的消息
|
||||
rule = command("help", "帮助")
|
||||
# 匹配 "/help.cmd" 开头的消息
|
||||
rule = command(("help", "cmd"))
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_command
|
||||
|
||||
matcher = on_command("help", aliases={"帮助"})
|
||||
```
|
||||
|
||||
此外,`command` 响应规则默认允许消息命令与参数间不加空格,如果需要严格匹配命令与参数间的空白符,可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串,默认为 `False`。如果为 `True`,则命令与参数间必须有任意个数的空白符;如果为字符串,则命令与参数间必须有且与给定字符串一致的空白符。
|
||||
|
||||
```python
|
||||
rule = command("help", force_whitespace=True)
|
||||
rule = command("help", force_whitespace=" ")
|
||||
```
|
||||
|
||||
命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。
|
||||
|
||||
### `shell_command`
|
||||
|
||||
`shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配,如果匹配成功,则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行,在此基础上添加了消息序列 `Message` 支持。
|
||||
|
||||
例如,我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import shell_command, ArgumentParser
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
|
||||
rule = shell_command("cmd", parser=parser)
|
||||
```
|
||||
|
||||
更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数,这样 `shell_command` 将不会解析参数,但会提供参数列表 `argv`。
|
||||
|
||||
直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_shell_command
|
||||
from nonebot.rule import ArgumentParser
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
|
||||
matcher = on_shell_command("cmd", parser=parser)
|
||||
```
|
||||
|
||||
参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。
|
||||
|
||||
### `regex`
|
||||
|
||||
`regex` 响应规则用于匹配消息是否与指定正则表达式匹配。
|
||||
|
||||
:::tip 提示
|
||||
正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 模式来确保匹配开头。
|
||||
:::
|
||||
|
||||
例如,我们可以创建一个匹配消息中包含字母并且忽略大小写的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import regex
|
||||
|
||||
rule = regex(r"[a-z]+", flags=re.IGNORECASE)
|
||||
```
|
||||
|
||||
也可以直接使用辅助函数新建一个响应器:
|
||||
|
||||
```python
|
||||
from nonebot import on_regex
|
||||
|
||||
matcher = on_regex(r"[a-z]+", flags=re.IGNORECASE)
|
||||
```
|
||||
|
||||
正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。
|
||||
|
||||
### `to_me`
|
||||
|
||||
`to_me` 响应规则用于匹配事件是否与机器人相关。
|
||||
|
||||
例如:
|
||||
|
||||
```python
|
||||
from nonebot.rule import to_me
|
||||
|
||||
rule = to_me()
|
||||
```
|
||||
|
||||
### `is_type`
|
||||
|
||||
`is_type` 响应规则用于匹配事件类型是否为指定类型(或者一系列类型)。
|
||||
|
||||
例如,我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则:
|
||||
|
||||
```python
|
||||
from nonebot.rule import is_type
|
||||
from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent
|
||||
|
||||
rule = is_type(PrivateMessageEvent, GroupMessageEvent)
|
||||
```
|
||||
|
||||
## 响应器组
|
||||
|
||||
为了更方便的管理一系列功能相近的响应器,NoneBot 提供了两种响应器组,它们可以帮助我们进行响应器的统一管理。
|
||||
|
||||
### `CommandGroup`
|
||||
|
||||
`CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。
|
||||
|
||||
例如,我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令,他们具有相同的优先级:
|
||||
|
||||
```python
|
||||
from nonebot import CommandGroup
|
||||
|
||||
group = CommandGroup("cmd", priority=10)
|
||||
|
||||
cmd = group.command(tuple())
|
||||
sub_cmd = group.command("sub")
|
||||
help_cmd = group.command("help")
|
||||
```
|
||||
|
||||
### `MatcherGroup`
|
||||
|
||||
`MatcherGroup` 可以用于管理一系列具有相同属性的响应器。
|
||||
|
||||
例如,我们创建一个具有相同响应规则的响应器组:
|
||||
|
||||
```python
|
||||
from nonebot.rule import to_me
|
||||
from nonebot import MatcherGroup
|
||||
|
||||
group = MatcherGroup(rule=to_me())
|
||||
|
||||
matcher1 = group.on_message()
|
||||
matcher2 = group.on_message()
|
||||
```
|
@@ -1,95 +0,0 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 自定义事件响应器的响应权限
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 40
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 权限控制
|
||||
|
||||
**权限控制**是机器人在实际应用中需要解决的重点问题之一,NoneBot2 提供了灵活的权限控制机制——`Permission`,接下来我们将简单说明。
|
||||
|
||||
## 应用
|
||||
|
||||
如同 `Rule` 一样,`Permission` 可以在[定义事件响应器](../tutorial/plugin/create-matcher.md)时添加 `permission` 参数来加以应用,这样 NoneBot2 会在事件响应时检测事件主体的权限。下面我们以 `SUPERUSER` 为例,对该机制的应用做一下介绍。
|
||||
|
||||
```python
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot import on_command
|
||||
|
||||
matcher = on_command("测试超管", permission=SUPERUSER)
|
||||
|
||||
|
||||
@matcher.handle()
|
||||
async def _():
|
||||
await matcher.send("超管命令测试成功")
|
||||
|
||||
|
||||
@matcher.got("key1", "超管提问")
|
||||
async def _():
|
||||
await matcher.send("超管命令 got 成功")
|
||||
```
|
||||
|
||||
在这段代码中,我们事件响应器指定了 `SUPERUSER` 这样一个权限,那么机器人只会响应超级管理员的 `测试超管` 命令,并且会响应该超级管理员的连续对话。
|
||||
|
||||
:::tip 提示
|
||||
在这里需要强调的是,`Permission` 与 `Rule` 的表现并不相同, `Rule` 只会在初次响应时生效,在余下的对话中并没有限制事件;但是 `Permission` 会持续生效,在连续对话中一直对事件主体加以限制。
|
||||
:::
|
||||
|
||||
## 进阶
|
||||
|
||||
`Permission` 除了可以在注册事件响应器时加以应用,还可以在编写事件处理函数 `handler` 时主动调用,我们可以利用这个特性在一个 `handler` 里对不同权限的事件主体进行区别响应,下面我们以 OneBot 适配器中的 `GROUP_ADMIN`(普通管理员非群主)和 `GROUP_OWNER` 为例,说明下怎么进行主动调用。
|
||||
|
||||
```python
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
|
||||
from nonebot.adapters.onebot.v11 import GROUP_ADMIN, GROUP_OWNER
|
||||
|
||||
matcher = on_command("测试权限")
|
||||
|
||||
@matcher.handle()
|
||||
async def _(bot: Bot, event: GroupMessageEvent):
|
||||
if await GROUP_ADMIN(bot, event):
|
||||
await matcher.send("管理员测试成功")
|
||||
elif await GROUP_OWNER(bot, event):
|
||||
await matcher.send("群主测试成功")
|
||||
else:
|
||||
await matcher.send("群员测试成功")
|
||||
```
|
||||
|
||||
在这段代码里,我们并没有对命令的权限指定,这个命令会响应所有在群聊中的 `测试权限` 命令,但是在 `handler` 里,我们对两个 `Permission` 进行主动调用,从而可以对不同的角色进行不同的响应。
|
||||
|
||||
## 自定义
|
||||
|
||||
如同 `Rule` 一样,`Permission` 也是由非负数个 `PermissionChecker` 组成的,但只需其中一个返回 `True` 时就会匹配成功。下面是自定义 `PermissionChecker` 和 `Permission` 的示例:
|
||||
|
||||
```python
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.permission import Permission
|
||||
|
||||
async def async_checker(bot: Bot, event: Event) -> bool:
|
||||
return True
|
||||
|
||||
def sync_checker(bot: Bot, event: Event) -> bool:
|
||||
return True
|
||||
|
||||
def check(arg1, arg2):
|
||||
|
||||
async def _checker(bot: Bot, event: Event) -> bool:
|
||||
return bool(arg1 + arg2)
|
||||
|
||||
return Permission(_checker)
|
||||
```
|
||||
|
||||
`Permission` 和 `PermissionChecker` 之间可以使用 `|`(或符号)互相组合:
|
||||
|
||||
```python
|
||||
from nonebot.permission import Permission
|
||||
|
||||
Permission(async_checker1) | sync_checker | async_checker2
|
||||
```
|
||||
|
||||
同样地,如果想用 `Permission(*checkers)` 包裹构造 `Permission`,函数必须是异步的;但是在利用 `|`(或符号)连接构造时,NoneBot2 会自动包裹同步函数为异步函数。
|
97
website/docs/advanced/plugin-info.md
Normal file
97
website/docs/advanced/plugin-info.md
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: 填写与获取插件相关的信息
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 30
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 插件信息
|
||||
|
||||
NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。同时,我们也可以通过 NoneBot 的插件系统来获取相关信息,例如插件的名称、使用方法,用于收集帮助信息等。下面我们将介绍如何为插件添加元数据,以及如何获取插件信息。
|
||||
|
||||
## 插件元数据
|
||||
|
||||
在 NoneBot 中,插件 [`Plugin`](../api/plugin/plugin.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。
|
||||
|
||||
现在,假设我们有一个插件 `example`, 它的模块结构如下:
|
||||
|
||||
```tree {4-6} title=Project
|
||||
📦 awesome-bot
|
||||
├── 📂 awesome_bot
|
||||
│ └── 📂 plugins
|
||||
| └── 📂 example
|
||||
| ├── 📜 __init__.py
|
||||
| └── 📜 config.py
|
||||
├── 📜 pyproject.toml
|
||||
└── 📜 README.md
|
||||
```
|
||||
|
||||
我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示:
|
||||
|
||||
```python {1,5-11} title=example/__init__.py
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from .config import Config
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="示例插件",
|
||||
description="这是一个示例插件",
|
||||
usage="没什么用",
|
||||
config=Config,
|
||||
extra={},
|
||||
)
|
||||
```
|
||||
|
||||
我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有两个可选的属性。`config` 属性用于指定插件的[配置类](../appendices/config.mdx#插件配置),`extra` 属性,它是一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。
|
||||
|
||||
请注意,这里的**插件名称**是供使用者或机器人用户查看的,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。
|
||||
|
||||
## 获取插件信息
|
||||
|
||||
NoneBot 提供了多种获取插件对象的方法,例如获取当前所有已导入的插件:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
plugins: set[Plugin] = nonebot.get_loaded_plugins()
|
||||
```
|
||||
|
||||
也可以通过插件索引名称获取插件对象:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
plugin: Plugin | None = nonebot.get_plugin("example")
|
||||
```
|
||||
|
||||
或者通过模块路径获取插件对象:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
plugin: Plugin | None = nonebot.get_plugin_by_module_name("awesome_bot.plugins.example")
|
||||
```
|
||||
|
||||
如果需要获取所有当前声明的插件名称(可能还未加载),可以使用 `get_available_plugin_names` 函数:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
plugin_names: set[str] = nonebot.get_available_plugin_names()
|
||||
```
|
||||
|
||||
插件对象 `Plugin` 中包含了多个属性:
|
||||
|
||||
- `name`:插件索引名称
|
||||
- `module`:插件模块
|
||||
- `module_name`:插件模块路径
|
||||
- `manager`:插件管理器
|
||||
- `matcher`:插件中定义的事件响应器
|
||||
- `parent_plugin`:插件的父插件
|
||||
- `sub_plugins`:插件的子插件集合
|
||||
- `metadata`:插件元数据
|
||||
|
||||
通过这些属性以及插件元数据,我们就可以收集所需要的插件信息了。
|
41
website/docs/advanced/plugin-nesting.md
Normal file
41
website/docs/advanced/plugin-nesting.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 编写与加载嵌套插件
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 40
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 嵌套插件
|
||||
|
||||
NoneBot 支持嵌套插件,即一个插件可以包含其他插件。通过这种方式,我们可以将一个大型插件拆分成多个功能子插件,使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。
|
||||
|
||||
## 创建嵌套插件
|
||||
|
||||
我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时,选择直接通过模板创建一个嵌套插件:
|
||||
|
||||
```bash
|
||||
$ nb plugin create
|
||||
[?] 插件名称: parent
|
||||
[?] 使用嵌套插件? (y/N) Y
|
||||
[?] 输出目录: awesome_bot/plugins
|
||||
```
|
||||
|
||||
或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。
|
||||
|
||||
## 已有插件
|
||||
|
||||
如果你已经有一个插件,想要在其中嵌套加载子插件,可以在插件的 `__init__.py` 中添加如下代码:
|
||||
|
||||
```python title=parent/__init__.py
|
||||
import nonebot
|
||||
from pathlib import Path
|
||||
|
||||
sub_plugins = nonebot.load_plugins(
|
||||
str(Path(__file__).parent.joinpath("plugins").resolve())
|
||||
)
|
||||
```
|
||||
|
||||
这样,`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系,你可以在 `parent` 的插件信息中看到这些子插件的信息,也可以在子插件信息中看到它们的父插件信息。
|
@@ -1,72 +0,0 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
description: 发布插件到 NoneBot2 商店
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 发布插件
|
||||
|
||||
## 前注
|
||||
|
||||
本章节仅包含插件发布流程指导,插件开发请查阅[**创建插件**](../tutorial/plugin/introduction.md)章节与[**Plugin API 文档**](../api/plugin/index.md)。
|
||||
|
||||
## 插件发布流程
|
||||
|
||||
### 发布到 PyPI
|
||||
|
||||
您可以选择自己喜欢的方式将插件发布到 [**PyPI**](https://pypi.org/),如使用 [**setuptools**](https://pypi.org/project/setuptools/) 或 [**Poetry**](https://pypi.org/project/poetry/)。
|
||||
|
||||
发布时,请您为自己的插件取一个清晰易懂的名字。通常而言,一款 NoneBot2 插件名称使用 `nonebot-plugin-` 作为前缀(如`nonebot-plugin-foo`),以 `nonebot_plugin_` 作为包名的前缀(如`nonebot_plugin_foo`),这并非强制规范,而是为了防止与其他 PyPI 包产生冲突,所以我们推荐您在没有特殊需求的情况下这样做。
|
||||
|
||||
发布后,请确保您的插件已能公开的从 PyPI 访问到,试着检查您的插件在 PyPI 的地址,如 `https://pypi.org/project/<您的 NoneBot2 插件项目名>`。
|
||||
|
||||
### 托管您的插件源代码
|
||||
|
||||
将插件源代码及相关构建文件(如 `pyproject.toml` 或 `setup.py` 等与 PyPI 包构建相关的文件)托管在公开代码仓。
|
||||
|
||||
请确保您的代码仓库地址能够被正确的访问,检查您的插件在代码仓的地址,如 `https://github.com/<您的 Github 用户名>/<您的插件 Github 项目名>`。
|
||||
|
||||
### 申请发布到 NoneBot2 插件商店
|
||||
|
||||
完成在 PyPI 的插件发布流程与源代码托管流程后,请您前往 [**NoneBot2 商店**](https://v2.nonebot.dev/store.html)页面,切换到**插件**页签,点击**发布插件**按钮。
|
||||
|
||||

|
||||
|
||||
如图所示,在弹出的插件信息提交表单内,填入您所要发布的相应插件信息:
|
||||
|
||||
```text
|
||||
插件名称:您的 NoneBot2 插件名称
|
||||
插件介绍:为您的插件提供的简短介绍信息
|
||||
PyPI 项目名:您的插件所在的 PyPI Project 名,如 nonebot-plugin-xxxx
|
||||
import 包名:您的插件通过 Python 导入时使用的包名,如 nonebot_plugin_xxxx
|
||||
仓库/主页:您的插件托管地址,如 https://github.com/<您的 Github 用户名>/nonebot-plugin-xxxx
|
||||
标签:一个或多个可选颜色的 TAG,每填写一个点击添加标签,若要删除,点击标签即可;标签长度不超过 10 字符,标签个数不超过 3 个
|
||||
特定标签内容 Adapter:点击 Type 的 Adapter,将创建一个 a: 开头的标签,填入内容以指定您插件使用的 adapter
|
||||
特定标签内容 Topic:点击 Type 的 Topic,将创建一个 t: 开头的标签,填入内容以指定您插件的主题
|
||||
```
|
||||
|
||||

|
||||
|
||||
完成填写后,请点击**发布**按钮,这将自动在[**NoneBot2**](https://github.com/nonebot/nonebot2)代码仓内创建发布您的插件的对应 Issue。
|
||||
|
||||
### 等待插件发布处理
|
||||
|
||||
您的插件发布 Issue 创建后,将会经过 _NoneBot2 Publish Bot_ 的检查,以确保插件信息正确无误。
|
||||
|
||||
若您的插件发布 Issue 未通过检查,您可以**直接修改** Issue 内容以更新发布请求。_NoneBot2 Publish Bot_ 在您修改 Issue 内容后将会自动重新执行检查。您无需关闭、重新提交发布申请。
|
||||
|
||||
之后,NoneBot2 的维护者们将会对插件进行进一步的检查,以确保用户能够正常安装并使用该插件。
|
||||
|
||||
完成这些步骤后,您的插件将会被合并到 [**NoneBot2 商店**](https://v2.nonebot.dev/store.html),而您也将成为 [**NoneBot2 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)中的一员。
|
||||
|
||||
## 完成
|
||||
|
||||
恭喜您,经过上述的发布流程,您的插件已经成功发布到 NoneBot2 商店了。
|
||||
|
||||
此时,您可以在 [**NoneBot2 商店**](https://v2.nonebot.dev/store.html)的插件页签查找到您的插件。同时,欢迎您成为 [**NoneBot2 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)!
|
||||
|
||||
**Congratulations!**
|
37
website/docs/advanced/requiring.md
Normal file
37
website/docs/advanced/requiring.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
description: 使用其他插件提供的功能
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 50
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 跨插件访问
|
||||
|
||||
NoneBot 插件化系统的设计使得插件之间可以功能独立、各司其职,我们可以更好地维护和扩展插件。但是,有时候我们可能需要在不同插件之间调用功能。NoneBot 生态中就有一类插件,它们专为其他插件提供功能支持,如:[定时任务插件](../best-practice/scheduler.md)、[数据存储插件](../best-practice/data-storing.md)等。这时候我们就需要在插件之间进行跨插件访问。
|
||||
|
||||
## 插件跟踪
|
||||
|
||||
由于 NoneBot 插件系统通过 [Import Hooks](https://docs.python.org/3/reference/import.html#import-hooks) 的方式实现插件加载与跟踪管理,因此我们**不能**在 NoneBot 跟踪插件前进行模块 import,这会导致插件加载失败。即,我们不能在使用 NoneBot 提供的加载插件方法前,直接使用 `import` 语句导入插件。
|
||||
|
||||
对于在项目目录下的插件,我们通常直接使用 `load_from_toml` 等方法一次性加载所有插件。由于这些插件已经被声明,即便插件导入顺序不同,NoneBot 也能正确跟踪插件。此时,我们不需要对跨插件访问进行特殊处理。但当我们使用了外部插件,如果没有事先声明或加载插件,NoneBot 并不会将其当作插件进行跟踪,可能会出现意料之外的错误出现。
|
||||
|
||||
简单来说,我们必须在 `import` 外部插件之前,确保依赖的外部插件已经被声明或加载。
|
||||
|
||||
## 插件依赖声明
|
||||
|
||||
NoneBot 提供了一种方法来确保我们依赖的插件已经被正确加载,即使用 `require` 函数。通过 `require` 函数,我们可以在当前插件中声明依赖的插件,NoneBot 会在加载当前插件时,检查依赖的插件是否已经被加载,如果没有,会尝试优先加载依赖的插件。
|
||||
|
||||
假设我们有一个插件 `a` 依赖于插件 `b`,我们可以在插件 `a` 中使用 `require` 函数声明其依赖于插件 `b`:
|
||||
|
||||
```python {3} title=a/__init__.py
|
||||
from nonebot import require
|
||||
|
||||
require("b")
|
||||
|
||||
from b import some_function
|
||||
```
|
||||
|
||||
其中,`require` 函数的参数为插件索引名称或者外部插件的模块名称。在完成依赖声明后,我们可以在插件 `a` 中直接导入插件 `b` 所提供的功能。
|
134
website/docs/advanced/routing.md
Normal file
134
website/docs/advanced/routing.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
sidebar_position: 9
|
||||
description: 添加服务端路由规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 100
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 添加路由
|
||||
|
||||
在[驱动器](./driver.md)一节中,我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行,那么我们就可以向驱动器添加路由规则,从而实现自定义的 API 接口等功能。在添加路由规则时,我们需要注意驱动器的类型,详情可以参考[选择驱动器](./driver.md#配置驱动器)。
|
||||
|
||||
NoneBot 中,我们可以通过两种途径向驱动器添加路由规则:
|
||||
|
||||
1. 通过 NoneBot 的兼容层建立路由规则。
|
||||
2. 直接向 ASGI 应用添加路由规则。
|
||||
|
||||
这两种途径各有优劣,前者可以在各种服务端型驱动器下运行,但并不能直接使用 ASGI 应用框架提供的特性与功能;后者直接使用 ASGI 应用,更自由、功能完整,但只能在特定类型驱动器下运行。
|
||||
|
||||
在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断:
|
||||
|
||||
```python {3}
|
||||
from nonebot import get_driver
|
||||
from nonebot.drivers import ReverseDriver
|
||||
|
||||
can_use = isinstance(get_driver(), ReverseDriver)
|
||||
```
|
||||
|
||||
## 通过兼容层添加路由
|
||||
|
||||
NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServerSetup`,分别用于定义 HTTP 服务端和 WebSocket 服务端的路由规则。
|
||||
|
||||
### HTTP 路由
|
||||
|
||||
`HTTPServerSetup` 具有四个属性:
|
||||
|
||||
- `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。
|
||||
- `method`:请求方法。类型为 `str`。
|
||||
- `name`:路由名称,不可重复。类型为 `str`。
|
||||
- `handle_func`:路由处理函数。类型为 `Callable[[Request], Awaitable[Response]]`。
|
||||
|
||||
例如,我们添加一个 `/hello` 的路由,当请求方法为 `GET` 时,返回 `200 OK` 以及返回体信息:
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
from nonebot.drivers import URL, Request, Response, HTTPServerSetup
|
||||
|
||||
async def hello(request: Request) -> Response:
|
||||
return Response(200, content="Hello, world!")
|
||||
|
||||
if isinstance((driver := get_driver()), ReverseDriver):
|
||||
driver.setup_http_server(
|
||||
HTTPServerSetup(
|
||||
path=URL("/hello"),
|
||||
method="GET",
|
||||
name="hello",
|
||||
handle_func=hello,
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
对于 `Request` 和 `Response` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。
|
||||
|
||||
### WebSocket 路由
|
||||
|
||||
`WebSocketServerSetup` 具有三个属性:
|
||||
|
||||
- `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。
|
||||
- `name`:路由名称,不可重复。类型为 `str`。
|
||||
- `handle_func`:路由处理函数。类型为 `Callable[[WebSocket], Awaitable[Any]]`。
|
||||
|
||||
例如,我们添加一个 `/ws` 的路由,发送所有接收到的数据:
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
from nonebot.drivers import URL, WebSocket, WebSocketServerSetup
|
||||
|
||||
async def ws_handler(ws: WebSocket):
|
||||
await ws.accept()
|
||||
try:
|
||||
while True:
|
||||
data = await ws.receive()
|
||||
await ws.send(data)
|
||||
except WebSocketClosed as e:
|
||||
# handle closed
|
||||
...
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
await websocket.close()
|
||||
# do some cleanup
|
||||
|
||||
if isinstance((driver := get_driver()), ReverseDriver):
|
||||
driver.setup_websocket_server(
|
||||
WebSocketServerSetup(
|
||||
path=URL("/ws"),
|
||||
name="ws",
|
||||
handle_func=ws_handler,
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
对于 `WebSocket` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。
|
||||
|
||||
## 使用 ASGI 应用添加路由
|
||||
|
||||
### 获取 ASGI 应用
|
||||
|
||||
NoneBot 服务端类型的驱动器具有两个属性 `server_app` 和 `asgi`,分别对应驱动框架应用和 ASGI 应用。通常情况下,这两个应用是同一个对象。我们可以通过 `get_app()` 方法快速获取:
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
app = nonebot.get_app()
|
||||
asgi = nonebot.get_asgi()
|
||||
```
|
||||
|
||||
### 添加路由规则
|
||||
|
||||
在获取到了 ASGI 应用后,我们就可以直接使用 ASGI 应用框架提供的功能来添加路由规则了。这里我们以 [FastAPI](./driver.md#fastapi默认) 为例,演示如何添加路由规则。
|
||||
|
||||
在下面的代码中,我们添加了一个 `GET` 类型的 `/api` 路由,具体方法参考 [FastAPI 文档](https://fastapi.tiangolo.com/)。
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
from fastapi import FastAPI
|
||||
|
||||
app: FastAPI = nonebot.get_app()
|
||||
|
||||
@app.get("/api")
|
||||
async def custom_api():
|
||||
return {"message": "Hello, world!"}
|
||||
```
|
@@ -1,75 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: 自定义事件响应器的响应规则
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 30
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 自定义匹配规则
|
||||
|
||||
机器人在实际应用中,往往会接收到多种多样的事件类型,NoneBot2 提供了可自定义的匹配规则 ── `Rule`。在[定义事件响应器](../tutorial/plugin/create-matcher.md#创建事件响应器)中,已经介绍了多种内置的事件响应器,接下来我们将说明自定义匹配规则的基本用法。
|
||||
|
||||
## 创建匹配规则
|
||||
|
||||
匹配规则可以是一个 `Rule` 对象,也可以是一个 `RuleChecker` 类型。`Rule` 是多个 `RuleChecker` 的集合,只有当所有 `RuleChecker` 检查通过时匹配成功。`RuleChecker` 是一个返回值为 `Bool` 类型的依赖函数,即,`RuleChecker` 支持依赖注入。
|
||||
|
||||
### 创建 `RuleChecker`
|
||||
|
||||
```python {1-2}
|
||||
async def user_checker(event: Event) -> bool:
|
||||
return event.get_user_id() == "123123"
|
||||
|
||||
matcher = on_message(rule=user_checker)
|
||||
```
|
||||
|
||||
在上面的代码中,我们定义了一个函数 `user_checker`,它检查事件的用户 ID 是否等于 `"123123"`。这个函数 `user_checker` 即为一个 `RuleChecker`。
|
||||
|
||||
### 创建 `Rule`
|
||||
|
||||
```python {1-2,4-5,7}
|
||||
async def user_checker(event: Event) -> bool:
|
||||
return event.get_user_id() == "123123"
|
||||
|
||||
async def message_checker(event: Event) -> bool:
|
||||
return event.get_plaintext() == "hello"
|
||||
|
||||
rule = Rule(user_checker, message_checker)
|
||||
matcher = on_message(rule=rule)
|
||||
```
|
||||
|
||||
在上面的代码中,我们定义了两个函数 `user_checker` 和 `message_checker`,它们检查事件的用户 ID 是否等于 `"123123"`,以及消息的内容是否等于 `"hello"`。随后,我们定义了一个 `Rule` 对象,它包含了这两个函数。
|
||||
|
||||
## 注册匹配规则
|
||||
|
||||
在[定义事件响应器](../tutorial/plugin/create-matcher.md#创建事件响应器)中,我们已经了解了如何事件响应器的组成。现在,我们仅需要将匹配规则注册到事件响应器中。
|
||||
|
||||
```python {4}
|
||||
async def user_checker(event: Event) -> bool:
|
||||
return event.get_user_id() == "123123"
|
||||
|
||||
matcher = on_message(rule=user_checker)
|
||||
```
|
||||
|
||||
在定义事件响应器的辅助函数中,都有一个 `rule` 参数,用于指定自定义的匹配规则。辅助函数会为你将自定义匹配规则与内置规则组合,并注册到事件响应器中。
|
||||
|
||||
## 合并匹配规则
|
||||
|
||||
在定义匹配规则时,我们往往希望将规则进行细分,来更好地复用规则。而在使用时,我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外,我们还可以对 `Rule` 对象进行合并。
|
||||
|
||||
```python {4-6}
|
||||
rule1 = Rule(foo_checker)
|
||||
rule2 = Rule(bar_checker)
|
||||
|
||||
rule = rule1 & rule2
|
||||
rule = rule1 & bar_checker
|
||||
rule = foo_checker & rule2
|
||||
```
|
||||
|
||||
同时,你也无需担心合并了一个 `None` 值,`Rule` 会忽略 `None` 值。
|
||||
|
||||
```python
|
||||
assert (rule & None) is rule
|
||||
```
|
@@ -1,39 +1,28 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
description: 在 NoneBot2 框架中添加 Hook 函数
|
||||
sidebar_position: 8
|
||||
description: 在特定的生命周期中执行代码
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 50
|
||||
weight: 90
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 钩子函数
|
||||
|
||||
[钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)
|
||||
> [钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。
|
||||
|
||||
> 钩子编程(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。
|
||||
|
||||
在 NoneBot2 中有一系列预定义的钩子函数,分为两类:**全局钩子函数**和**事件钩子函数**,这些钩子函数可以用装饰器的形式来使用。
|
||||
在 NoneBot 中有一系列预定义的钩子函数,可以分为两类:**全局钩子函数**和**事件处理钩子函数**,这些钩子函数可以用装饰器的形式来使用。
|
||||
|
||||
## 全局钩子函数
|
||||
|
||||
全局钩子函数是指 NoneBot2 针对其本身运行过程的钩子函数。
|
||||
全局钩子函数是指 NoneBot 针对其本身运行过程的钩子函数。
|
||||
|
||||
这些钩子函数是由其后端驱动 `Driver` 来运行的,故需要先获得全局 `Driver` 对象:
|
||||
|
||||
```python
|
||||
from nonebot import get_driver
|
||||
|
||||
|
||||
driver=get_driver()
|
||||
```
|
||||
|
||||
共分为六种函数:
|
||||
这些钩子函数是由驱动器来运行的,故需要先[获得全局驱动器](./driver.md#获取驱动器)。
|
||||
|
||||
### 启动准备
|
||||
|
||||
这个钩子函数会在 NoneBot2 启动时运行。
|
||||
这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。
|
||||
|
||||
```python
|
||||
@driver.on_startup
|
||||
@@ -43,7 +32,7 @@ async def do_something():
|
||||
|
||||
### 终止处理
|
||||
|
||||
这个钩子函数会在 NoneBot2 终止时运行。
|
||||
这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。
|
||||
|
||||
```python
|
||||
@driver.on_shutdown
|
||||
@@ -53,7 +42,7 @@ async def do_something():
|
||||
|
||||
### Bot 连接处理
|
||||
|
||||
这个钩子函数会在 `Bot` 通过 websocket 连接到 NoneBot2 时运行。
|
||||
这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。
|
||||
|
||||
```python
|
||||
@driver.on_bot_connect
|
||||
@@ -61,9 +50,9 @@ async def do_something(bot: Bot):
|
||||
pass
|
||||
```
|
||||
|
||||
### bot 断开处理
|
||||
### Bot 断开处理
|
||||
|
||||
这个钩子函数会在 `Bot` 断开与 NoneBot2 的 websocket 连接时运行。
|
||||
这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。
|
||||
|
||||
```python
|
||||
@driver.on_bot_disconnect
|
||||
@@ -71,101 +60,82 @@ async def do_something(bot: Bot):
|
||||
pass
|
||||
```
|
||||
|
||||
### bot api 调用钩子
|
||||
## 事件处理钩子函数
|
||||
|
||||
这个钩子函数会在 `Bot` 调用 API 时运行。
|
||||
|
||||
```python
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
@Bot.on_calling_api
|
||||
async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):
|
||||
pass
|
||||
```
|
||||
|
||||
### bot api 调用后钩子
|
||||
|
||||
这个钩子函数会在 `Bot` 调用 API 后运行。
|
||||
|
||||
```python
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
@Bot.on_called_api
|
||||
async def handle_api_result(bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any):
|
||||
pass
|
||||
```
|
||||
|
||||
## 事件钩子函数
|
||||
|
||||
这些钩子函数指的是影响 NoneBot2 进行**事件处理**的函数, 这些函数可以认为跟普通的事件处理函数一样,接受相应的参数。
|
||||
|
||||
:::tip 提示
|
||||
关于**事件处理**的流程,可以在[这里](./README.md)查阅。
|
||||
:::
|
||||
|
||||
:::warning
|
||||
|
||||
1.在事件处理钩子函数中,与 `matcher` 运行状态相关的函数将不可用,如 `matcher.finish()`
|
||||
|
||||
2.如果需要在事件处理钩子函数中打断整个对话的执行,请参考以下范例:
|
||||
|
||||
```python
|
||||
from nonebot.exception import IgnoredException
|
||||
|
||||
|
||||
@event_preprocessor
|
||||
async def do_something():
|
||||
raise IgnoredException("reason")
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
共分为四种函数:
|
||||
这些钩子函数指的是影响 NoneBot 进行**事件处理**的函数, 这些函数可以跟普通的事件处理函数一样接受相应的参数。
|
||||
|
||||
### 事件预处理
|
||||
|
||||
这个钩子函数会在 `Event` 上报到 NoneBot2 时运行
|
||||
这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。
|
||||
|
||||
```python
|
||||
from nonebot.message import event_preprocessor
|
||||
|
||||
@event_preprocessor
|
||||
async def do_something():
|
||||
async def do_something(event: Event):
|
||||
pass
|
||||
```
|
||||
|
||||
### 事件后处理
|
||||
|
||||
这个钩子函数会在 NoneBot2 处理 `Event` 后运行
|
||||
这个钩子函数会在 NoneBot 处理事件完成后运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。
|
||||
|
||||
```python
|
||||
from nonebot.message import event_postprocessor
|
||||
|
||||
@event_postprocessor
|
||||
async def do_something():
|
||||
async def do_something(event: Event):
|
||||
pass
|
||||
```
|
||||
|
||||
### 运行预处理
|
||||
|
||||
这个钩子函数会在 NoneBot2 运行 `matcher` 前运行。
|
||||
这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态。
|
||||
|
||||
```python
|
||||
from nonebot.message import run_preprocessor
|
||||
|
||||
@run_preprocessor
|
||||
async def do_something():
|
||||
async def do_something(event: Event, matcher: Matcher):
|
||||
pass
|
||||
```
|
||||
|
||||
### 运行后处理
|
||||
|
||||
这个钩子函数会在 NoneBot2 运行 `matcher` 后运行。
|
||||
这个钩子函数会在 NoneBot 运行事件响应器后运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态、运行中产生的异常。
|
||||
|
||||
```python
|
||||
from nonebot.message import run_postprocessor
|
||||
|
||||
@run_postprocessor
|
||||
async def do_something():
|
||||
async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]):
|
||||
pass
|
||||
```
|
||||
|
||||
### 平台接口调用钩子
|
||||
|
||||
这个钩子函数会在 `Bot` 对象调用平台接口时运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来阻止 `Bot` 对象调用平台接口并返回指定的结果。
|
||||
|
||||
```python
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.exception import MockApiException
|
||||
|
||||
@Bot.on_calling_api
|
||||
async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):
|
||||
if api == "send_msg":
|
||||
raise MockApiException(result={"message_id": 123})
|
||||
```
|
||||
|
||||
### 平台接口调用后钩子
|
||||
|
||||
这个钩子函数会在 `Bot` 对象调用平台接口后运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来忽略平台接口返回的结果并返回指定的结果。
|
||||
|
||||
```python
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.exception import MockApiException
|
||||
|
||||
@Bot.on_called_api
|
||||
async def handle_api_result(
|
||||
bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any
|
||||
):
|
||||
if not exception and api == "send_msg":
|
||||
raise MockApiException(result={**result, "message_id": 123})
|
||||
```
|
||||
|
@@ -1,147 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 在 NoneBot2 中使用定时任务
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 定时任务
|
||||
|
||||
[APScheduler](https://apscheduler.readthedocs.io/en/3.x/) —— Advanced Python Scheduler
|
||||
|
||||
> Advanced Python Scheduler (APScheduler) is a Python library that lets you schedule your Python code to be executed later, either just once or periodically. You can add new jobs or remove old ones on the fly as you please. If you store your jobs in a database, they will also survive scheduler restarts and maintain their state. When the scheduler is restarted, it will then run all the jobs it should have run while it was offline.
|
||||
|
||||
## 从 NoneBot v1 迁移
|
||||
|
||||
`APScheduler` 作为 NoneBot v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。NoneBot2 已将 `APScheduler` 独立为 nonebot_plugin_apscheduler 插件,你可以在[商店](/store)中找到它。
|
||||
|
||||
相比于 NoneBot v1,NoneBot v2 只需要安装插件并修改 `scheduler` 的导入方式即可完成迁移。
|
||||
|
||||
## 安装插件
|
||||
|
||||
### 通过 nb-cli
|
||||
|
||||
如正在使用 nb-cli 构建项目,你可以从插件市场复制安装命令或手动输入以下命令以添加 nonebot_plugin_apscheduler。
|
||||
|
||||
```bash
|
||||
nb plugin install nonebot_plugin_apscheduler
|
||||
```
|
||||
|
||||
:::tip 提示
|
||||
nb-cli 默认通过 PyPI 安装,你可以添加命令参数 `-i [mirror]` 或 `--index [mirror]` 以使用镜像源安装。
|
||||
:::
|
||||
|
||||
### 通过 Poetry
|
||||
|
||||
执行以下命令以添加 nonebot_plugin_apscheduler
|
||||
|
||||
```bash
|
||||
poetry add nonebot-plugin-apscheduler
|
||||
```
|
||||
|
||||
:::tip 提示
|
||||
由于稍后我们将使用 `nonebot.require()` 方法进行声明依赖,所以无需额外的 `nonebot.load_plugin()`
|
||||
:::
|
||||
|
||||
## 快速上手
|
||||
|
||||
1. 在需要设置定时任务的插件中,通过 `nonebot.require` 声明插件依赖
|
||||
2. 从 nonebot_plugin_apscheduler 导入 `scheduler` 对象
|
||||
3. 在该对象的基础上,根据 `APScheduler` 的使用方法进一步配置定时任务
|
||||
|
||||
将上述步骤归纳为最小实现的代码如下:
|
||||
|
||||
```python {3,5}
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_apscheduler")
|
||||
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
@scheduler.scheduled_job("cron", hour="*/2", id="xxx", args=[1], kwargs={"arg2": 2})
|
||||
async def run_every_2_hour(arg1, arg2):
|
||||
pass
|
||||
|
||||
scheduler.add_job(run_every_day_from_program_start, "interval", days=1, id="xxx")
|
||||
```
|
||||
|
||||
## 分步进行
|
||||
|
||||
### 导入 scheduler 对象
|
||||
|
||||
为了使插件能够实现定时任务,需要先将 `scheduler` 对象导入插件。
|
||||
|
||||
NoneBot2 提供了 `nonebot.require` 方法来实现声明插件依赖,确保 `nonebot_plugin_apscheduler` 插件可以在使用之前被导入。声明完成即可直接通过 import 导入 `scheduler` 对象。
|
||||
|
||||
NoneBot2 使用的 `scheduler` 对象为 `AsyncIOScheduler` 。
|
||||
|
||||
```python {3,5}
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_apscheduler")
|
||||
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
```
|
||||
|
||||
### 编写定时任务
|
||||
|
||||
由于本部分为标准的通过 `APScheduler` 配置定时任务,有关指南请参阅 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html#adding-jobs)。
|
||||
|
||||
### 配置插件选项
|
||||
|
||||
根据项目的 `.env` 文件设置,向 `.env.*` 或 `bot.py` 文件添加 nonebot_plugin_apscheduler 的可选配置项
|
||||
|
||||
:::warning 注意
|
||||
`.env.*` 文件的编写应遵循 NoneBot2 对 `.env.*` 文件的编写要求
|
||||
:::
|
||||
|
||||
#### `apscheduler_autostart`
|
||||
|
||||
类型:`bool`
|
||||
|
||||
默认值:`True`
|
||||
|
||||
是否自动启动 `APScheduler`。
|
||||
|
||||
对于大多数情况,我们需要在 NoneBot2 项目被启动时启动定时任务,则此处设为 `true`
|
||||
|
||||
##### 在 `.env` 中添加
|
||||
|
||||
```bash
|
||||
APSCHEDULER_AUTOSTART=true
|
||||
```
|
||||
|
||||
##### 在 `bot.py` 中添加
|
||||
|
||||
```python
|
||||
nonebot.init(apscheduler_autostart=True)
|
||||
```
|
||||
|
||||
#### `apscheduler_config`
|
||||
|
||||
类型:`dict`
|
||||
|
||||
默认值:`{"apscheduler.timezone": "Asia/Shanghai"}`
|
||||
|
||||
`APScheduler` 相关配置。修改/增加其中配置项需要确保 `prefix: apscheduler`。
|
||||
|
||||
对于 `APScheduler` 的相关配置,请参阅 [scheduler-config](https://apscheduler.readthedocs.io/en/3.x/userguide.html#scheduler-config) 和 [BaseScheduler](https://apscheduler.readthedocs.io/en/3.x/modules/schedulers/base.html#apscheduler.schedulers.base.BaseScheduler)
|
||||
|
||||
> 官方文档在绝大多数时候能提供最准确和最具时效性的指南
|
||||
|
||||
##### 在 `.env` 中添加
|
||||
|
||||
```bash
|
||||
APSCHEDULER_CONFIG={"apscheduler.timezone": "Asia/Shanghai"}
|
||||
```
|
||||
|
||||
##### 在 `bot.py` 中添加
|
||||
|
||||
```python
|
||||
nonebot.init(apscheduler_config={
|
||||
"apscheduler.timezone": "Asia/Shanghai"
|
||||
})
|
||||
```
|
59
website/docs/advanced/session-updating.md
Normal file
59
website/docs/advanced/session-updating.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
description: 控制会话响应对象
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 会话更新
|
||||
|
||||
在 NoneBot 中,在某个事件响应器对事件响应后,即是进入了会话状态,会话状态会持续到整个事件响应流程结束。会话过程中,机器人可以与用户进行多次交互。每次需要等待用户事件时,NoneBot 将会复制一个新的临时事件响应器,并更新该事件响应器使其响应当前会话主体的消息,这个过程称为会话更新。
|
||||
|
||||
会话更新分为两部分:**更新[事件响应器类型](./matcher.md#事件响应器类型)**和**更新[事件触发权限](./matcher.md#事件触发权限)**。
|
||||
|
||||
## 更新事件响应器类型
|
||||
|
||||
通常情况下,与机器人用户进行的会话都是通过消息事件进行的,因此会话更新后的默认响应事件类型为 `message`。如果希望接收一个特定类型的消息,比如 `notice` 等,我们需要自定义响应事件类型更新函数。响应事件类型更新函数是一个 `Dependent`,可以使用依赖注入。
|
||||
|
||||
```python {3-5}
|
||||
foo = on_message()
|
||||
|
||||
@foo.type_updater
|
||||
async def _() -> str:
|
||||
return "notice"
|
||||
```
|
||||
|
||||
在注册了上述响应事件类型更新函数后,当我们需要等待用户事件时,将只会响应 `notice` 类型的事件。如果希望在会话过程中的不同阶段响应不同类型的事件,我们就需要使用更复杂的逻辑来更新响应事件类型(如:根据会话状态),这里将不再展示。
|
||||
|
||||
## 更新事件触发权限
|
||||
|
||||
会话通常是由机器人与用户进行的一对一交互,因此会话更新后的默认触发权限为当前事件的会话 ID。这个会话 ID 由协议适配器生成,通常由用户 ID 和群 ID 等组成。如果希望实现更复杂的会话功能(如:多用户同时参与的会话),我们需要自定义触发权限更新函数。触发权限更新函数是一个 `Dependent`,可以使用依赖注入。
|
||||
|
||||
```python {5-7}
|
||||
from nonebot.permission import User
|
||||
|
||||
foo = on_message()
|
||||
|
||||
@foo.permission_updater
|
||||
async def _(event: Event, matcher: Matcher) -> Permission:
|
||||
return Permission(User.from_event(event, perm=matcher.permission))
|
||||
```
|
||||
|
||||
上述权限更新函数是默认的权限更新函数,它将会话的触发权限更新为当前事件的会话 ID。如果我们希望响应多个用户的消息,我们可以如下修改:
|
||||
|
||||
```python {5-7}
|
||||
from nonebot.permission import USER
|
||||
|
||||
foo = on_message()
|
||||
|
||||
@foo.permission_updater
|
||||
async def _(matcher: Matcher) -> Permission:
|
||||
return USER("session1", "session2", perm=matcher.permission)
|
||||
```
|
||||
|
||||
请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。
|
||||
|
||||
我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store)。
|
@@ -1,106 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 使用 NoneBug 测试机器人
|
||||
slug: /advanced/unittest/
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 70
|
||||
category: advanced
|
||||
---
|
||||
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
|
||||
# 单元测试
|
||||
|
||||
[单元测试](https://zh.wikipedia.org/wiki/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95)
|
||||
|
||||
> 在计算机编程中,单元测试(Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。
|
||||
|
||||
NoneBot2 使用 [Pytest](https://docs.pytest.org) 单元测试框架搭配 [NoneBug](https://github.com/nonebot/nonebug) 插件进行单元测试,通过直接与事件响应器/适配器等交互简化测试流程,更易于编写。
|
||||
|
||||
## 安装 NoneBug
|
||||
|
||||
安装 NoneBug 时,Pytest 会作为依赖被一起安装。
|
||||
|
||||
要运行 NoneBug,还需要额外安装 Pytest 异步插件 `pytest-asyncio` 或 `anyio`,文档将以 `pytest-asyncio` 为例。
|
||||
|
||||
```bash
|
||||
poetry add nonebug pytest-asyncio --dev
|
||||
# 也可以通过 pip 安装
|
||||
pip install nonebug pytest-asyncio
|
||||
```
|
||||
|
||||
:::tip 提示
|
||||
建议首先阅读 [Pytest 文档](https://docs.pytest.org) 理解相关术语。
|
||||
:::
|
||||
|
||||
## 加载插件
|
||||
|
||||
我们可以使用 Pytest **Fixtures** 来加载插件,下面是一个示例:
|
||||
|
||||
```python title=conftest.py
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
import pytest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def load_plugins(nonebug_init: None) -> Set["Plugin"]:
|
||||
import nonebot # 这里的导入必须在函数内
|
||||
|
||||
# 加载插件
|
||||
return nonebot.load_plugins("awesome_bot/plugins")
|
||||
```
|
||||
|
||||
此 Fixture 的 [`nonebug_init`](https://github.com/nonebot/nonebug/blob/master/nonebug/fixture.py) 形参也是一个 Fixture,用于初始化 NoneBug。
|
||||
|
||||
Fixture 名称 `load_plugins` 可以修改为其他名称,文档以 `load_plugins` 为例。需要加载插件时,在测试函数添加形参 `load_plugins` 即可。加载完成后即可使用 `import` 导入事件响应器。
|
||||
|
||||
## 测试流程
|
||||
|
||||
Pytest 会在函数开始前通过 Fixture `app`(nonebug_app) **初始化 NoneBug** 并返回 `App` 对象。
|
||||
|
||||
:::warning 警告
|
||||
所有从 `nonebot` 导入模块的函数都需要首先初始化 NoneBug App,否则会发生不可预料的问题。
|
||||
|
||||
在每个测试函数结束时,NoneBug 会自动销毁所有与 NoneBot 相关的资源。所有与 NoneBot 相关的 import 应在函数内进行导入。
|
||||
:::
|
||||
|
||||
随后使用 `test_matcher` 等测试方法获取到 `Context` 上下文,通过上下文管理提供的方法(如 `should_call_send` 等)预定义行为。
|
||||
|
||||
在上下文管理器关闭时,`Context` 会调用 `run_test` 方法按照预定义行为对事件响应器进行断言(如:断言事件响应和 API 调用等)。
|
||||
|
||||
## 测试样例
|
||||
|
||||
:::tip 提示
|
||||
将从 `utils` 导入的 `make_fake_message`,`make_fake_event` 替换为对应平台的消息/事件类型。
|
||||
|
||||
将 `load_example` 替换为加载插件的 Fixture 名称。
|
||||
:::
|
||||
|
||||
使用 NoneBug 的 `test_matcher` 可以模拟出一个事件流程。如下是一个简单的示例:
|
||||
|
||||
import WeatherSource from "!!raw-loader!@site/../tests/examples/weather.py";
|
||||
import WeatherTest from "!!raw-loader!@site/../tests/test_examples/test_weather.py";
|
||||
|
||||
<CodeBlock className="language-python" title="test_weather.py">
|
||||
{WeatherTest}
|
||||
</CodeBlock>
|
||||
|
||||
<details>
|
||||
<summary>示例插件</summary>
|
||||
<CodeBlock className="language-python" title="examples/weather.py">
|
||||
{WeatherSource}
|
||||
</CodeBlock>
|
||||
</details>
|
||||
|
||||
在测试用例编写完成后 ,可以使用下面的命令运行单元测试。
|
||||
|
||||
```bash
|
||||
pytest test_weather.py
|
||||
```
|
@@ -1,162 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
description: 测试适配器
|
||||
---
|
||||
|
||||
# 测试适配器
|
||||
|
||||
通常来说,测试适配器需要测试这三项。
|
||||
|
||||
1. 测试连接
|
||||
2. 测试事件转化
|
||||
3. 测试 API 调用
|
||||
|
||||
## 注册适配器
|
||||
|
||||
任何的适配器都需要注册才能起作用。
|
||||
|
||||
我们可以使用 Pytest 的 Fixtures,在测试开始前初始化 NoneBot 并**注册适配器**。
|
||||
|
||||
我们假设适配器为 `nonebot.adapters.test`。
|
||||
|
||||
```python {20,21} title=conftest.py
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
# 如果适配器采用 nonebot.adapters monospace 则需要使用此hook方法正确添加路径
|
||||
@pytest.fixture
|
||||
def import_hook():
|
||||
import nonebot.adapters
|
||||
|
||||
nonebot.adapters.__path__.append( # type: ignore
|
||||
str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve())
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
async def init_adapter(app: App, import_hook):
|
||||
import nonebot
|
||||
from nonebot.adapters.test import Adapter
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
driver.register_adapter(Adapter)
|
||||
```
|
||||
|
||||
## 测试连接
|
||||
|
||||
任何的适配器的连接方式主要有以下 4 种:
|
||||
|
||||
1. 反向 HTTP(WebHook)
|
||||
2. 反向 WebSocket
|
||||
3. ~~正向 HTTP(尚未实现)~~
|
||||
4. ~~正向 WebSocket(尚未实现)~~
|
||||
|
||||
NoneBug 的 `test_server` 方法可以供我们测试反向连接方式。
|
||||
|
||||
`test_server` 的 `get_client` 方法可以获取 HTTP/WebSocket 客户端。
|
||||
|
||||
我们假设适配器 HTTP 上报地址为 `/test/http`,反向 WebSocket 地址为 `/test/ws`,上报机器人 ID
|
||||
使用请求头 `Bot-ID` 来演示如何通过 NoneBug 测试适配器。
|
||||
|
||||
```python {8,16,17,19-22,26,34,36-39} title=test_connection.py
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"endpoints", ["/test/http"]
|
||||
)
|
||||
async def test_http(app: App, init_adapter, endpoints: str):
|
||||
import nonebot
|
||||
|
||||
async with app.test_server() as ctx:
|
||||
client = ctx.get_client()
|
||||
|
||||
body = {"post_type": "test"}
|
||||
headers = {"Bot-ID": "test"}
|
||||
|
||||
resp = await client.post(endpoints, json=body, headers=headers)
|
||||
assert resp.status_code == 204 # 检测状态码是否正确
|
||||
bots = nonebot.get_bots()
|
||||
assert "test" in bots # 检测是否连接成功
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"endpoints", ["/test/ws"]
|
||||
)
|
||||
async def test_ws(app: App, init_adapter, endpoints: str):
|
||||
import nonebot
|
||||
|
||||
async with app.test_server() as ctx:
|
||||
client = ctx.get_client()
|
||||
|
||||
headers = {"Bot-ID": "test"}
|
||||
|
||||
async with client.websocket_connect(endpoints, headers=headers) as ws:
|
||||
bots = nonebot.get_bots()
|
||||
assert "test" in bots # 检测是否连接成功
|
||||
```
|
||||
|
||||
## 测试事件转化
|
||||
|
||||
事件转化就是将原始数据反序列化为 `Event` 对象的过程。
|
||||
|
||||
测试事件转化就是测试反序列化是否按照预期转化为对应 `Event` 类型。
|
||||
|
||||
下面将以 `dict_to_event` 作为反序列化方法,`type` 为 `test` 的事件类型为 `TestEvent` 来演示如何测试事件转化。
|
||||
|
||||
```python {8,9} title=test_event.py
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_event(app: App, init_adapter):
|
||||
from nonebot.adapters.test import Adapter, TestEvent
|
||||
|
||||
event = Adapter.dict_to_event({"post_type": "test"}) # 反序列化原始数据
|
||||
assert isinstance(event, TestEvent) # 断言类型是否与预期一致
|
||||
```
|
||||
|
||||
## 测试 API 调用
|
||||
|
||||
将消息序列化为原始数据并由适配器发送到协议端叫做 API 调用。
|
||||
|
||||
测试 API 调用就是调用 API 并验证返回与预期返回是否一致。
|
||||
|
||||
```python {16-18,23-32} title=test_call_api.py
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ws(app: App, init_adapter):
|
||||
import nonebot
|
||||
|
||||
async with app.test_server() as ctx:
|
||||
client = ctx.get_client()
|
||||
|
||||
headers = {"Bot-ID": "test"}
|
||||
|
||||
async def call_api():
|
||||
bot = nonebot.get_bot("test")
|
||||
return await bot.test_api()
|
||||
|
||||
async with client.websocket_connect("/test/ws", headers=headers) as ws:
|
||||
task = asyncio.create_task(call_api())
|
||||
|
||||
# received = await ws.receive_text()
|
||||
# received = await ws.receive_bytes()
|
||||
received = await ws.receive_json()
|
||||
assert received == {"action": "test_api"} # 检测调用是否与预期一致
|
||||
response = {"result": "test"}
|
||||
# await ws.send_text(...)
|
||||
# await ws.send_bytes(...)
|
||||
await ws.send_json(response, mode="bytes")
|
||||
result = await task
|
||||
assert result == response # 检测返回是否与预期一致
|
||||
```
|
@@ -1,122 +0,0 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 测试事件响应处理
|
||||
---
|
||||
|
||||
# 测试事件响应处理行为
|
||||
|
||||
除了 `send`,事件响应器还有其他的操作,我们也需要对它们进行测试,下面我们将定义如下事件响应器操作的预期行为对对应的事件响应器操作进行测试。
|
||||
|
||||
## should_finished
|
||||
|
||||
定义事件响应器预期结束当前事件的整个处理流程。
|
||||
|
||||
适用事件响应器操作:[`finish`](../../tutorial/plugin/matcher-operation.md#finish)。
|
||||
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
<details>
|
||||
<summary>示例插件</summary>
|
||||
|
||||
```python title=example.py
|
||||
from nonebot import on_message
|
||||
|
||||
foo = on_message()
|
||||
|
||||
@foo.handle()
|
||||
async def _():
|
||||
await foo.finish("test")
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
```python {13}
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App, load_plugins):
|
||||
from awesome_bot.plugins.example import foo
|
||||
|
||||
async with app.test_matcher(foo) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_fake_event()() # 此处替换为平台事件
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "test", True)
|
||||
ctx.should_finished()
|
||||
```
|
||||
|
||||
## should_paused
|
||||
|
||||
定义事件响应器预期立即结束当前事件处理依赖并等待接收一个新的事件后进入下一个事件处理依赖。
|
||||
|
||||
适用事件响应器操作:[`pause`](../../tutorial/plugin/matcher-operation.md#pause)。
|
||||
|
||||
<details>
|
||||
<summary>示例插件</summary>
|
||||
|
||||
```python title=example.py
|
||||
from nonebot import on_message
|
||||
|
||||
foo = on_message()
|
||||
|
||||
@foo.handle()
|
||||
async def _():
|
||||
await foo.pause("test")
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
```python {13}
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App, load_plugins):
|
||||
from awesome_bot.plugins.example import foo
|
||||
|
||||
async with app.test_matcher(foo) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_fake_event()() # 此处替换为平台事件
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "test", True)
|
||||
ctx.should_paused()
|
||||
```
|
||||
|
||||
## should_rejected
|
||||
|
||||
定义事件响应器预期立即结束当前事件处理依赖并等待接收一个新的事件后再次执行当前事件处理依赖。
|
||||
|
||||
适用事件响应器操作:[`reject`](../../tutorial/plugin/matcher-operation.md#reject)
|
||||
、[`reject_arg`](../../tutorial/plugin/matcher-operation.md#reject_arg)
|
||||
和 [`reject_receive`](../../tutorial/plugin/matcher-operation.md#reject_receive)。
|
||||
|
||||
<details>
|
||||
<summary>示例插件</summary>
|
||||
|
||||
```python title=example.py
|
||||
from nonebot import on_message
|
||||
|
||||
foo = on_message()
|
||||
|
||||
@foo.got("key")
|
||||
async def _():
|
||||
await foo.reject("test")
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
```python {13}
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App, load_plugins):
|
||||
from awesome_bot.plugins.example import foo
|
||||
|
||||
async with app.test_matcher(foo) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_fake_event()() # 此处替换为平台事件
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "test", True)
|
||||
ctx.should_rejected()
|
||||
```
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user