mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-06 18:56:45 +00:00
Compare commits
356 Commits
v2.0.0-bet
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
e5e69c2726 | ||
|
7c7ea613e9 | ||
|
bb1b94e5e3 | ||
|
8420add975 | ||
|
2192e8cb6d | ||
|
48ccef2f06 | ||
|
c6bc24efc2 | ||
|
63f8d78d20 | ||
|
db36c262db | ||
|
732a13b692 | ||
|
71bf1d1147 | ||
|
6e98ac031c | ||
|
9a49354ddd | ||
|
455752bd92 | ||
|
2a51b07229 | ||
|
732b5b0b1b | ||
|
a0dcc7753c | ||
|
12942f2d50 | ||
|
192d094f54 | ||
|
bb02d50837 | ||
|
bc8c65d0d8 | ||
|
9447b1f462 | ||
|
c03b0c73cb | ||
|
19f4c01ad3 | ||
|
9bd07b9ced | ||
|
fe5cf5624c | ||
|
a14c38300e | ||
|
9e908d5b3f | ||
|
f1ab95489c | ||
|
3c42e26e27 | ||
|
c248b8c354 | ||
|
0ecea50778 | ||
|
33d4d01d51 | ||
|
1667440c64 | ||
|
141527238c | ||
|
e2d0453741 | ||
|
0849df1c76 | ||
|
c4d45c087a | ||
|
be15cfabcc | ||
|
dddbeb389f | ||
|
bdfaf4840f | ||
|
b37b1380a3 | ||
|
d8ed5c2e80 | ||
|
4bc391c066 | ||
|
5aa6138bf3 | ||
|
fc78b9c547 | ||
|
118874080d | ||
|
cf2137a1a9 | ||
|
14b145b58d | ||
|
1aba737cbd | ||
|
fe38b1f17f | ||
|
776651284f | ||
|
068cc3a7ea | ||
|
0e7e88cfa2 | ||
|
c120f9be70 | ||
|
563436b38e | ||
|
386be6cbb6 | ||
|
b5e29533d8 | ||
|
beb19adad5 | ||
|
e2289c78b0 | ||
|
d3f261eb34 | ||
|
d54c2e6bf4 | ||
|
6fdebc4912 | ||
|
f1ffac5ca7 | ||
|
12716ee79a | ||
|
42413281bb | ||
|
a67eda4c80 | ||
|
9dbea871b8 | ||
|
4181f4ca77 | ||
|
fe4a33d19b | ||
|
58d8815f39 | ||
|
b80083fed5 | ||
|
4ba17d900a | ||
|
f11970132c | ||
|
c91c9380a7 | ||
|
06ee47edcd | ||
|
a82ce00a4b | ||
|
e0902eeb58 | ||
|
b754ac2fbc | ||
|
1ae0a654bf | ||
|
b85348f648 | ||
|
7b06469a30 | ||
|
a62a49d477 | ||
|
e2f96a0b5c | ||
|
28c22b7511 | ||
|
c027e4d2ce | ||
|
e38bb2b530 | ||
|
19a9a3c3c5 | ||
|
4c8f5059db | ||
|
9ab1acf1e7 | ||
|
175acd38eb | ||
|
4241eb538c | ||
|
c55b32b9f9 | ||
|
2082e5f5c2 | ||
|
8485a356e7 | ||
|
f6f70fb435 | ||
|
c3ce4f76d1 | ||
|
db90a9ebab | ||
|
3962a98743 | ||
|
3ce23bc593 | ||
|
3238a82042 | ||
|
ed1f920088 | ||
|
c34b3439fa | ||
|
bd2225c43c | ||
|
381234725e | ||
|
a5ee6ac401 | ||
|
79fd12768d | ||
|
c0ab86f91b | ||
|
ae21bfdd0e | ||
|
6cfa21b15c | ||
|
fa3ed2b58c | ||
|
579839f2a4 | ||
|
daa95d02ac | ||
|
33c261c802 | ||
|
558e073cfa | ||
|
c52637a3b8 | ||
|
e3d1f572ed | ||
|
af86b96974 | ||
|
ddc42f7be8 | ||
|
41d50641ad | ||
|
6feed0610b | ||
|
fe43cc92a5 | ||
|
f6fb3b3970 | ||
|
d8ea7f1e6f | ||
|
dd55650f0a | ||
|
20f414d0de | ||
|
5924f1e7ac | ||
|
2fbd44eef9 | ||
|
0adf4d6934 | ||
|
2c91a4c7f1 | ||
|
625c72ab0d | ||
|
449a2c5f96 | ||
|
9e64f3f8ab | ||
|
9b45b77894 | ||
|
e890453870 | ||
|
abcea78fcc | ||
|
80594cffb6 | ||
|
6d4c5cbc2d | ||
|
d295e9ef6b | ||
|
2ad46bf97a | ||
|
70ddc634f6 | ||
|
540629aa7c | ||
|
4f4369c712 | ||
|
c6633bc9af | ||
|
1c099b4d13 | ||
|
abe1e29fd9 | ||
|
e8b9963ef3 | ||
|
90f7c153cb | ||
|
983a5930c6 | ||
|
ff65f10da9 | ||
|
1710d009bb | ||
|
049d988574 | ||
|
97fa0b4fe9 | ||
|
cd42385a43 | ||
|
56f99b7f0b | ||
|
91c5056c97 | ||
|
5e970a291f | ||
|
42a49a20aa | ||
|
a4a329cf87 | ||
|
dc074f35d5 | ||
|
591107870e | ||
|
94b19b4833 | ||
|
1a91371410 | ||
|
a77664297d | ||
|
b889d2352e | ||
|
17e09267e0 | ||
|
bbf734b2d1 | ||
|
7ab9e85dc0 | ||
|
71bfb42fe0 | ||
|
aeaea54ac1 | ||
|
49437daf10 | ||
|
a93401e3b4 | ||
|
e87861983b | ||
|
2d81d54d93 | ||
|
e145d99335 | ||
|
7e3a58a0e8 | ||
|
11b6e1ba98 | ||
|
34186830ab | ||
|
b98be416e4 | ||
|
aaae928026 | ||
|
1e43b4df10 | ||
|
505b4d46d0 | ||
|
95331bbb22 | ||
|
f028575f2f | ||
|
252e3de459 | ||
|
6449b1e9fd | ||
|
5334f11902 | ||
|
76ffcf14e8 | ||
|
c8f25db6f6 | ||
|
4845ca10a4 | ||
|
eec27a267a | ||
|
3870f0084d | ||
|
06b36ec278 | ||
|
dcfa25c486 | ||
|
14953f5161 | ||
|
16f69b045b | ||
|
533e99418c | ||
|
f989710cd6 | ||
|
e1534f2205 | ||
|
532aee5e71 | ||
|
fb047a4987 | ||
|
af799aa846 | ||
|
91f4daa722 | ||
|
42fa47263a | ||
|
39fd544651 | ||
|
e5bb30e2b5 | ||
|
a6d8f18cf0 | ||
|
47d843ddca | ||
|
74542d30e0 | ||
|
e12445be2f | ||
|
d8eb7d311b | ||
|
7ac14bab03 | ||
|
e2621b4448 | ||
|
2f3324ce0c | ||
|
494b9c625d | ||
|
f20cf785ce | ||
|
82803ff90f | ||
|
d38b5602a6 | ||
|
adb5fd8ca0 | ||
|
977e1de077 | ||
|
537866db95 | ||
|
9ffd78dda3 | ||
|
599da5158e | ||
|
2b64e8266c | ||
|
8ccf10954a | ||
|
09b9a626e6 | ||
|
1d221fddab | ||
|
524ed419c2 | ||
|
536e75f994 | ||
|
130c2ed5c0 | ||
|
3c8e705bb0 | ||
|
87e5e15b52 | ||
|
a2abc5a714 | ||
|
de434b3072 | ||
|
614f005373 | ||
|
36efa3f441 | ||
|
78a90ef7aa | ||
|
2e5df56d38 | ||
|
a230e98052 | ||
|
45e2e6c280 | ||
|
fcdb05a7e2 | ||
|
8c6d5a2d1f | ||
|
8735b61a8d | ||
|
02de6fd266 | ||
|
06f8dde33c | ||
|
0fe3e4fb16 | ||
|
56c6f6a471 | ||
|
58e69f7884 | ||
|
4ebbf7638c | ||
|
8d7507c8f2 | ||
|
c1c720756b | ||
|
a1be18f7f4 | ||
|
e2fcfa902e | ||
|
b54f4c8d4c | ||
|
4cf07ca2e0 | ||
|
5b3dd8f020 | ||
|
9bb291e95b | ||
|
492c0947c3 | ||
|
547d50ad76 | ||
|
f7600a8a62 | ||
|
9bd380a3bb | ||
|
8600687f7d | ||
|
1d98ea1961 | ||
|
998db949da | ||
|
dea1b8c6fa | ||
|
d348f544b1 | ||
|
b1559eee42 | ||
|
edc7183c22 | ||
|
ea539345cc | ||
|
03d60dd0be | ||
|
48003a779f | ||
|
2203b82b09 | ||
|
9fc2f7c02e | ||
|
6ee295bfd7 | ||
|
6b067f0865 | ||
|
339c25638b | ||
|
e6cd3e57f5 | ||
|
69fcda5658 | ||
|
88f8614cfc | ||
|
273b302ef2 | ||
|
e86bab74d3 | ||
|
b1df360900 | ||
|
2c271da965 | ||
|
5db9c1e232 | ||
|
b05ce41b1d | ||
|
bda27c78b9 | ||
|
6f0e27ee6d | ||
|
2c89409667 | ||
|
5adc5ce1cd | ||
|
cad2f90b8a | ||
|
b3d246cfb1 | ||
|
a8a6eb8c93 | ||
|
86a73011b1 | ||
|
9fb089bf08 | ||
|
72d993921f | ||
|
db764f7e9e | ||
|
1767f7a388 | ||
|
fc45c67d97 | ||
|
87885bd878 | ||
|
d75a04b31a | ||
|
12313204e1 | ||
|
4573235583 | ||
|
4293bdf21f | ||
|
baae3e48de | ||
|
24df95ae4a | ||
|
6b50a57348 | ||
|
6920ec3a11 | ||
|
6586f28f6a | ||
|
192c8da09c | ||
|
1fba27d9b8 | ||
|
0f0dc0a818 | ||
|
3c3a250180 | ||
|
03d33f3bdc | ||
|
1147d67f1a | ||
|
98e5956d44 | ||
|
999a6a0e10 | ||
|
ff2675b527 | ||
|
daa026cfd7 | ||
|
d4d3962177 | ||
|
1d6a333b49 | ||
|
9c0e05c615 | ||
|
258bdbe403 | ||
|
c48ddaf0a2 | ||
|
9dd989c627 | ||
|
3d84844a58 | ||
|
d63d434e0f | ||
|
5216a5b8f2 | ||
|
4107affb9b | ||
|
b898303e4d | ||
|
43aebd9c93 | ||
|
fcda5c37d7 | ||
|
1412385e51 | ||
|
00fab9143e | ||
|
5e17d4c2f9 | ||
|
d7de928f22 | ||
|
83ff7a3a6c | ||
|
6f9c9eb740 | ||
|
6272dfd46a | ||
|
ab78769822 | ||
|
004a308765 | ||
|
98e0ec27ee | ||
|
a491d842db | ||
|
722fc6c6e1 | ||
|
a5fffe2a4f | ||
|
5f1f84327d | ||
|
3d8ac3e789 | ||
|
c05eea2b67 | ||
|
a6299bec8f | ||
|
513c14ee78 | ||
|
987e44e1d0 | ||
|
e7937e5a06 | ||
|
962c71ea4e | ||
|
04e9a50bc1 | ||
|
4bd1b92e9f | ||
|
f737bb899c | ||
|
9f12404338 |
15
.devcontainer/Dockerfile
Normal file
15
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/codespaces-linux/.devcontainer/base.Dockerfile
|
||||
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/universal:2-focal
|
||||
|
||||
# ** [Optional] Uncomment this section to install additional packages. **
|
||||
# USER root
|
||||
#
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
USER codespace
|
||||
|
||||
# [Required] Poetry
|
||||
RUN curl -sSL https://install.python-poetry.org | python - -y
|
||||
RUN poetry config virtualenvs.in-project true
|
98
.devcontainer/devcontainer.json
Normal file
98
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,98 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/codespaces-linux
|
||||
{
|
||||
"name": "GitHub Codespaces (Default)",
|
||||
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"python.defaultInterpreterPath": "/opt/python/latest/bin/python",
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
||||
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
|
||||
"python.analysis.diagnosticMode": "workspace",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"lldb.executable": "/usr/bin/lldb",
|
||||
"files.exclude": {
|
||||
"**/__pycache__": true
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/target/**": true,
|
||||
"**/__pycache__": true
|
||||
}
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.isort",
|
||||
"ms-python.black-formatter",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"remoteUser": "codespace",
|
||||
|
||||
"overrideCommand": false,
|
||||
|
||||
"mounts": [
|
||||
"source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume"
|
||||
],
|
||||
|
||||
"runArgs": [
|
||||
"--cap-add=SYS_PTRACE",
|
||||
"--security-opt",
|
||||
"seccomp=unconfined",
|
||||
"--privileged",
|
||||
"--init"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// "oryx build" will automatically install your dependencies and attempt to build your project
|
||||
"postCreateCommand": "poetry install && poetry run pre-commit install && yarn install"
|
||||
}
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -4,11 +4,11 @@ contact_links:
|
||||
url: https://discussions.nonebot.dev/
|
||||
about: Ask questions about nonebot
|
||||
- name: Plugin Publish
|
||||
url: https://v2.nonebot.dev/store.html
|
||||
url: https://v2.nonebot.dev/store
|
||||
about: Publish your plugin to nonebot homepage and nb-cli
|
||||
- name: Adapter Publish
|
||||
url: https://v2.nonebot.dev/store.html
|
||||
url: https://v2.nonebot.dev/store
|
||||
about: Publish your adapter to nonebot homepage and nb-cli
|
||||
- name: Bot Publish
|
||||
url: https://v2.nonebot.dev/store.html
|
||||
url: https://v2.nonebot.dev/store
|
||||
about: Publish your bot to nonebot homepage and nb-cli
|
||||
|
11
.github/actions/setup-python/action.yml
vendored
11
.github/actions/setup-python/action.yml
vendored
@@ -10,17 +10,22 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/setup-python@v2
|
||||
- id: python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
architecture: "x64"
|
||||
|
||||
- uses: Gr1N/setup-poetry@v7
|
||||
|
||||
- id: poetry-cache
|
||||
run: echo "::set-output name=dir::$(poetry config virtualenvs.path)"
|
||||
shell: bash
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pypoetry/virtualenvs
|
||||
key: ${{ runner.os }}-poetry-${{ inputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||
path: ${{ steps.poetry-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-poetry-${{ steps.python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||
|
||||
- run: poetry install -E all
|
||||
shell: bash
|
||||
|
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
31
.github/release-drafter.yml
vendored
31
.github/release-drafter.yml
vendored
@@ -1,37 +1,34 @@
|
||||
header: |
|
||||
### Documentation
|
||||
|
||||
See: https://v2.nonebot.dev
|
||||
template: |
|
||||
### 💫 Changes
|
||||
|
||||
$CHANGES
|
||||
template: $CHANGES
|
||||
category-template: "### $TITLE"
|
||||
name-template: "Release v$RESOLVED_VERSION 🌈"
|
||||
tag-template: "v$RESOLVED_VERSION"
|
||||
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
|
||||
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
|
||||
change-template: "- $TITLE [@$AUTHOR](https://github.com/$AUTHOR) ([#$NUMBER]($URL))"
|
||||
change-title-escapes: '\<&'
|
||||
exclude-labels:
|
||||
- "dependencies"
|
||||
- "skip-changelog"
|
||||
categories:
|
||||
- title: "💥 Breaking Changes"
|
||||
- title: "💥 破坏性变更"
|
||||
labels:
|
||||
- "Breaking"
|
||||
- title: "🚀 Features"
|
||||
- title: "🚀 新功能"
|
||||
labels:
|
||||
- "feature"
|
||||
- "enhancement"
|
||||
- title: "🐛 Bug Fixes"
|
||||
- title: "🐛 Bug 修复"
|
||||
labels:
|
||||
- "fix"
|
||||
- "bugfix"
|
||||
- "bug"
|
||||
- title: "📝 Documentation"
|
||||
- title: "📝 文档"
|
||||
labels:
|
||||
- "documentation"
|
||||
- title: "🍻 Plugin Publish"
|
||||
- title: "💫 杂项"
|
||||
- title: "🍻 插件发布"
|
||||
label: "Plugin"
|
||||
- title: "🍻 Bot Publish"
|
||||
- title: "🍻 机器人发布"
|
||||
label: "Bot"
|
||||
- title: "🍻 Adapter Publish"
|
||||
- title: "🍻 适配器发布"
|
||||
label: "Adapter"
|
||||
version-resolver:
|
||||
major:
|
||||
|
8
.github/workflows/codecov.yml
vendored
8
.github/workflows/codecov.yml
vendored
@@ -4,13 +4,15 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test Coverage
|
||||
runs-on: ${{ matrix.os }}
|
||||
concurrency:
|
||||
group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
@@ -21,7 +23,7 @@ jobs:
|
||||
PYTHON_VERSION: ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Python environment
|
||||
uses: ./.github/actions/setup-python
|
||||
@@ -34,7 +36,7 @@ jobs:
|
||||
poetry run pytest -n auto --cov-report xml
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
env_vars: OS,PYTHON_VERSION
|
||||
files: ./tests/coverage.xml
|
||||
|
9
.github/workflows/publish-bot.yml
vendored
9
.github/workflows/publish-bot.yml
vendored
@@ -1,12 +1,9 @@
|
||||
name: NoneBot2 Publish Bot
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
issues:
|
||||
types: [opened, reopened, edited]
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
@@ -15,12 +12,12 @@ jobs:
|
||||
name: nonebot2 publish bot
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: NoneBot2 Publish Bot
|
||||
uses: nonebot/nonebot2-publish-bot@main
|
||||
uses: docker://ghcr.io/nonebot/nonebot2-publish-bot:main
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
config: >
|
||||
|
34
.github/workflows/release-drafter.yml
vendored
34
.github/workflows/release-drafter.yml
vendored
@@ -14,24 +14,46 @@ jobs:
|
||||
update-release-draft:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: pull-request-changelog
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Setup Node Environment
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
id: release-drafter
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# TODO
|
||||
# - name: Update Changelog
|
||||
# run: |
|
||||
# echo ${{ steps.release-drafter.outputs.body }}
|
||||
- name: Update Changelog
|
||||
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||
with:
|
||||
changelog_file: website/src/pages/changelog.md
|
||||
latest_changes_position: '# 更新日志\n\n'
|
||||
latest_changes_title: "## 最近更新"
|
||||
replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )'
|
||||
changelog_body: ${{ steps.release-drafter.outputs.body }}
|
||||
commit_and_push: false
|
||||
|
||||
- name: Commit and Push
|
||||
run: |
|
||||
yarn prettier
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email github-actions[bot]@users.noreply.github.com
|
||||
git add .
|
||||
git diff-index --quiet HEAD || git commit -m ":memo: Update changelog"
|
||||
git push
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Python Environment
|
||||
uses: ./.github/actions/setup-python
|
||||
|
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: master
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
@@ -22,18 +22,26 @@ jobs:
|
||||
- name: Build API Doc
|
||||
uses: ./.github/actions/build-api-doc
|
||||
|
||||
- name: Archive Files
|
||||
run: yarn archive $(poetry version -s)
|
||||
- run: echo "TAG_NAME=v$(poetry version -s)" >> $GITHUB_ENV
|
||||
|
||||
# TODO
|
||||
- name: Archive Changelog
|
||||
run: cat CHANGELOG.md
|
||||
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||
with:
|
||||
changelog_file: website/src/pages/changelog.md
|
||||
archive_regex: '(?<=## )最近更新(?=\n)'
|
||||
archive_title: ${{ env.TAG_NAME }}
|
||||
commit_and_push: false
|
||||
|
||||
- name: Archive Files
|
||||
run: |
|
||||
yarn archive $(poetry version -s)
|
||||
yarn prettier
|
||||
|
||||
- name: Push Tag
|
||||
run: |
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email github-actions[bot]@users.noreply.github.com
|
||||
git add .
|
||||
git commit -m ":bookmark: Release $(poetry version -s)"
|
||||
git tag v$(poetry version -s)
|
||||
git tag ${{ env.TAG_NAME }}
|
||||
git push && git push --tags
|
||||
|
31
.github/workflows/website-deploy.yml
vendored
31
.github/workflows/website-deploy.yml
vendored
@@ -4,21 +4,16 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: website-deploy-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
if: github.event_name != 'push'
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Python Environment
|
||||
uses: ./.github/actions/setup-python
|
||||
@@ -35,27 +30,15 @@ jobs:
|
||||
- name: Get Branch Name
|
||||
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
|
||||
|
||||
- name: Get Deploy Name
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
echo "DEPLOY_NAME=${{ env.BRANCH_NAME }}" >> $GITHUB_ENV
|
||||
echo "PRODUCTION=${{ env.BRANCH_NAME == 'master' }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Get Deploy Name
|
||||
if: github.event_name != 'push'
|
||||
run: |
|
||||
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
|
||||
echo "PRODUCTION=false" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v1
|
||||
with:
|
||||
publish-dir: "./website/build"
|
||||
production-deploy: ${{ env.PRODUCTION }}
|
||||
production-deploy: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}"
|
||||
deploy-message: "Deploy ${{ env.BRANCH_NAME }}@${{ github.sha }}"
|
||||
enable-commit-comment: false
|
||||
alias: ${{ env.DEPLOY_NAME }}
|
||||
alias: ${{ env.BRANCH_NAME }}
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}
|
||||
|
45
.github/workflows/website-preview.yml
vendored
Normal file
45
.github/workflows/website-preview.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Site Deploy(Preview)
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
preview:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: pull-request-preview-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Setup Python Environment
|
||||
uses: ./.github/actions/setup-python
|
||||
|
||||
- name: Setup Node Environment
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Build API Doc
|
||||
uses: ./.github/actions/build-api-doc
|
||||
|
||||
- name: Build Doc
|
||||
run: yarn build
|
||||
|
||||
- name: Get Deploy Name
|
||||
run: |
|
||||
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v1
|
||||
with:
|
||||
publish-dir: "./website/build"
|
||||
production-deploy: false
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}"
|
||||
enable-commit-comment: false
|
||||
alias: ${{ env.DEPLOY_NAME }}
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}
|
4
.markdownlint.yaml
Normal file
4
.markdownlint.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
MD013: false
|
||||
MD024: # 重复标题
|
||||
siblings_only: true
|
||||
MD033: false # 允许 html
|
@@ -1,22 +1,32 @@
|
||||
default_install_hook_types: [pre-commit, prepare-commit-msg]
|
||||
ci:
|
||||
autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks"
|
||||
autofix_prs: true
|
||||
autoupdate_branch: dev
|
||||
autoupdate_schedule: weekly
|
||||
autoupdate_branch: master
|
||||
autoupdate_schedule: monthly
|
||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.1.0
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.5.1
|
||||
rev: v2.7.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, jsx, ts, tsx, markdown]
|
||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml]
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/nonebot/nonemoji
|
||||
rev: v0.1.2
|
||||
hooks:
|
||||
- id: nonemoji
|
||||
stages: [prepare-commit-msg]
|
||||
|
@@ -22,34 +22,61 @@ NoneBot2 还未进入正式版,欢迎在 Issue 中提议要加入哪些新功
|
||||
|
||||
NoneBot 使用 [poetry](https://python-poetry.org/) 管理项目依赖,由于 pre-commit 也经其管理,所以在此一并说明。
|
||||
|
||||
下面的命令能在已安装 poetry 和 npm 的情况下帮你快速配置开发环境。
|
||||
下面的命令能在已安装 poetry 和 yarn 的情况下帮你快速配置开发环境。
|
||||
|
||||
```sh
|
||||
```bash
|
||||
# 安装 python 依赖
|
||||
poetry install
|
||||
# 安装 pre-commit git hook
|
||||
pre-commit install
|
||||
npm -g i gitmoji-cli
|
||||
gitmoji -i
|
||||
```
|
||||
|
||||
### 使用 GitHub Codespaces(Dev Container)
|
||||
|
||||
使用 GitHub Codespaces 选择 `NoneBot2` 项目,然后选择 `.devcontainer/devcontainer.json` 配置即可。
|
||||
|
||||
### Commit 规范
|
||||
|
||||
请确保你的每一个 commit 都能清晰地描述其意图,一个 commit 尽量只有一个意图。
|
||||
|
||||
NoneBot 的 commit message 格式遵循 [gitmoji](https://gitmoji.dev/) 规范,在创建 commit 时请牢记这一点。
|
||||
|
||||
或者使用 [nonemoji](https://github.com/nonebot/nonemoji) 代替 git 进行 commit,nonemoji 已默认作为项目开发依赖安装。
|
||||
|
||||
```bash
|
||||
nonemoji commit [-e EMOJI] [-m MESSAGE] [-- ...]
|
||||
```
|
||||
|
||||
### 工作流概述
|
||||
|
||||
`master` 分支为 NoneBot 的开发分支,在任何情况下都请不要直接修改 `master` 分支,而是创建一个目标分支为 `nonebot:master` 的 Pull Request 来提交修改。Pull Request 标题请尽量更改成中文,以便自动生成更新日志。
|
||||
|
||||
如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `master` 分支发起 Pull Request,注意遵循先前提到的 commit message 规范创建 commit。我们将在 code review 通过后通过 squash merge 方式将您的贡献合并到主分支。
|
||||
|
||||
### 撰写文档
|
||||
|
||||
NoneBot2 的文档使用 [docusaurus](https://docusaurus.io/),它有一些 [Markdown 特性](https://docusaurus.io/zh-CN/docs/markdown-features) 可能会帮助到你。
|
||||
|
||||
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
|
||||
|
||||
如果你需要在本地预览修改后的文档,可以使用 yarn 安装文档依赖后启动 dev server,如下所示:
|
||||
|
||||
```sh
|
||||
```bash
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
|
||||
NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量写得简单易懂。
|
||||
|
||||
以下是比较重要的排版规范。目前 NoneBot2 文档中仍有部分文档不完全遵守此规范,如果在阅读时发现欢迎提交 PR。
|
||||
|
||||
1. 中文与英文、数字、半角符号之间需要有空格。例:`NoneBot2 是一个可扩展的 Python 异步机器人框架。`
|
||||
2. 若非英文整句,使用全角标点符号。例:`现在你可以看到机器人回复你:“Hello, World !”。`
|
||||
3. 直引号`「」`和弯引号`“”`都可接受,但同一份文件里应使用同种引号。
|
||||
4. **不要使用斜体**,你不需要一种与粗体不同的强调。除此之外,你也可以考虑使用 docusaurus 提供的[告示](https://docusaurus.io/zh-CN/docs/markdown-features/admonitions)功能。
|
||||
|
||||
这是社区创始人 richardchien 的中文排版规范,可供参考:<https://stdrc.cc/style-guides/chinese>。
|
||||
|
||||
如果你需要编辑器提示 Markdown 规范,可以安装 VSCode 上的 markdownlint 插件。
|
||||
|
||||
### 参与开发
|
||||
|
||||
NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 与 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 规范,请确保你的代码风格和项目已有的代码保持一致,变量命名清晰,有适当的注释与测试代码。
|
||||
@@ -63,9 +90,3 @@ NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/
|
||||
虽然对插件的内容没有严格限制,但我们还是建议在上架插件之前先查看商店有无功能一致的插件。如果你想要上架商店的插件功能与现有插件不完全重合,请在插件说明中补充其与现有插件的区别。
|
||||
|
||||
同时,如果你参考或基于他人发行的代码进行开发,请注意遵守各代码所使用的开源许可协议。
|
||||
|
||||
## Git 工作流
|
||||
|
||||
`dev` 分支为 NoneBot 的开发分支,如无特殊情况请将更改提交到该分支。
|
||||
|
||||
如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `dev` 分支发起 Pull Request,注意遵循先前提到的 commit message 规范创建 commit。
|
||||
|
22
README.md
22
README.md
@@ -29,27 +29,33 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
<img src="https://github.com/nonebot/nonebot2/actions/workflows/website-deploy.yml/badge.svg?branch=master&event=push" alt="site"/>
|
||||
</a>
|
||||
<a href="https://results.pre-commit.ci/latest/github/nonebot/nonebot2/master">
|
||||
<img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" />
|
||||
<img src="https://results.pre-commit.ci/badge/github/nonebot/nonebot2/master.svg" alt="pre-commit" />
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://onebot.dev/">
|
||||
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="cqhttp">
|
||||
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
|
||||
</a>
|
||||
<a href="https://onebot.dev/">
|
||||
<img src="https://img.shields.io/badge/OneBot-v12-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="onebot">
|
||||
</a>
|
||||
<a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
|
||||
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="ding">
|
||||
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="dingtalk">
|
||||
</a>
|
||||
<a href="https://core.telegram.org/bots/api">
|
||||
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram">
|
||||
<img src="https://img.shields.io/badge/telegram-Bot-lightgrey?style=social&logo=telegram" alt="telegram">
|
||||
</a>
|
||||
<a href="https://open.feishu.cn/document/home/index">
|
||||
<img src="https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAk1BMVEX///8zMzNJSUlSUlJcXFxtbW0zMzNLS0szMzMzMzNBQUGVlZUzMzM1NTU0NDQzMzMzMzM0NDQ0NDQ0NDQ3NzdDQ0M0NDQ2NjY4ODg9PT0zMzM0NDQ5OTk7OzszMzM0NDQ3NzczMzM0NDQ0NDQ0NDQ0NDQ3Nzc2NjY4ODg2NjY7Ozs0NDQ6Ojo6Ojo3Nzc4ODgzMzNGdMWJAAAAMHRSTlMD6h0TDgr8GNf0KQbvhLT45KKTmm4jwHVJLdLFQzbcjFTgzsq7rl58T2kyqD46Y1riMDRhAAAFr0lEQVR42uzZWXKiUACF4YMyqKAQhyjOc7STmLP/1bVlLukESIJ3sLGKbwFU/Q8HuIBKpVKpVCqVSqVSqVQqlUqlUvmNM10Mcfda/U6TPdw3e9lb8ayLO+bPniYu+amjNcPd8U7PFhML0RE5uCvnaY/5SVt0WFvckcu0vxjiYmDxbu5cl2mn9UVHRMa4B2LaP3RYKD1vL6adccRFLSLL/izxxbRz7UXHimdLlFdq2mlvnztYRznZh96cP3G/dkxQRrOnR5c/c5eiQ+S2UTbe/sHir9zD1w6+okz8aXvMItyRqE46ApSHmHYRYdLRoPCMcrAP3TkLC6fpDp5QAn/EtItqij3UG/zgQZH5aWc7ZqJjzA9jKFGf9ppXC3I6uMB/Mzh2mpQQ/Mnp4BSy1Kctx4pFx5qfhA4kqE87pCyrldfBDm6sLqat2mGnttXHDfkvYtryooHo2PCrFm5lcNw1qWr1XUeEm7BH3QYVRJNGcOmoietNmNKDWeKFnCo6b3Wc1drW/NsOLpFRqmmT4xgfPFw42Q7XhkFi2kq2DtKcR2Y8wpTacRdQ3aZYB59ggiOmrS6sFevgDNr9GW6pzRAZdsQsC3rV3x4i6uQha8+sB2h9am9c6rVBDj9ixr5k007rIs+CGV65pl3wXjRi2hrKYjFtM/rI02JaW3XaPYtGtZHHY9qL0lN7QuO2yBMzpenLTvtkZNos+AY1ZcpObtoLtWmrj6TNlCOuJqZ9M3PkaDBlIBHCmwpHyHpjSgMS2ryhcIqsmsWULmR0eTPhK7LsMdMOKHdJM+nw8E+8ZoYDOT3eRDDDuz6HNt7VeszaQtYDJch+38WRZ51TDO+0Y54hylzy0XHib2JI83c0zIoLd1hAeUus1jenQe2HQ79Dg6wB3i1d/uoNpS2JrulgHWqcRxqySjoObrFjfUlLVrVrOtiGMmdCA+ZJx8hlEa9QZ2+oXcNLOkIWEUAHe22sYxqykGdoUV//5w6eoKlkTI3Gdbx7CVmQB10lDWqzSTpemyyoAW28ubYO++oOLqBPbUUtJknHrMnCRihdyaOTdAQsLHSgtSTS2BEHLK4DvQYWFW2lOtiHZi3Fko6fXCjgNVooV0nHl7tMBP1aAaXtJDvYgwGxdMmzLzu1JUyYNSU7IAwiiZ8OJrxKlTzI38QnMORFoqSn8Fh9gikvIa/UVejgDMZMQ9mOOa8WwKCRyysslF6hn2HSwZX4+ew1KGEPCSZKhoqHMw9mLd1rO8aUMYZpexbRV/2AsYBxy7/t3NtuglAQheFR6wEPVEQtaq1WxQNqnfd/urY08QJFYHZS15D9vcHckMzOz/QWA9/3jqHrbmbr1bT10a90ncQcoiclgKY/Vq81q6P2JJqfI+NHPqdDSMRzsEtIXmYGcQcQk2fwKgHxTCIVJGMWwTu6sWGxPSFx+QpkOfz3QcYEJWQhtGsbR5aKCIrHInjXNsSDeITFZ6ELYZEMAnltY8AyawKz4KJAr21IBzkRmB6LOIRGOEhIaHYsciA0uxIshwa/DLQIzrAEy2HswBIBwck9yNOvbWT4YgHEU4zbEiyHsQsXhnmKccmxp2cbxvb8CyDbMBXwD4hsw1BQguUw9s4Mk20YOTFQtmHiDJVtGJhjZRtyEVi2ITbhnLBOMd5qOvqXwz9RFy3bkJpU0LINeTCsJdvIztHVZhsJo77SbOPG6FNltpFQqMxsE7hmS+9ymJxE7XKYUGupyzZS1Kbaso00tbWybONBTadyObyjPlaVbTycRFO28Uh9oyjbEJ/E2JImnVDXy1y6zpHvW5E2npJsI5unI9vIwVe3HKYZaMg2clkoyDby6Wl5mcv0Bp9t5DVEzzZyG4JnG/kdsLONArbQ2UYRlwZwtlHIsoGbbRSdRNtymGbf0LYcpgleQbMNwdUCbcthmrP2j++VjqdSy7Isy7Isy4LxDTcBnqEPd5jdAAAAAElFTkSuQmCC" alt="feishu">
|
||||
<img src="https://img.shields.io/badge/%E9%A3%9E%E4%B9%A6-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNyAyOUMyMSAyOSAyNSAyNi45MzM5IDI4IDIzLjQwNjVDMzYgMTQgNDEuNDI0MiAxNi44MTY2IDQ0IDE3Ljk5OThDMzguNSAyMC45OTk4IDQwLjUgMjkuNjIzMyAzMyAzNS45OTk4QzI4LjM4MiAzOS45MjU5IDIzLjQ5NDUgNDEuMDE0IDE5IDQxQzEyLjUyMzEgNDAuOTc5OSA2Ljg2MjI2IDM3Ljc2MzcgNCAzNS40MDYzVjE2Ljk5OTgiIHN0cm9rZT0iIzMzMyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNNS42NDgwOCAxNS44NjY5QzUuMDIyMzEgMTQuOTU2NyAzLjc3NzE1IDE0LjcyNjEgMi44NjY5NCAxNS4zNTE5QzEuOTU2NzMgMTUuOTc3NyAxLjcyNjE1IDE3LjIyMjggMi4zNTE5MiAxOC4xMzMxTDUuNjQ4MDggMTUuODY2OVpNMzYuMDAyMSAzNS43MzA5QzM2Ljk1OCAzNS4xNzc0IDM3LjI4NDMgMzMuOTUzOSAzNi43MzA5IDMyLjk5NzlDMzYuMTc3NCAzMi4wNDIgMzQuOTUzOSAzMS43MTU3IDMzLjk5NzkgMzIuMjY5MUwzNi4wMDIxIDM1LjczMDlaTTIuMzUxOTIgMTguMTMzMUM1LjI0MzUgMjIuMzM5IDEwLjc5OTIgMjguMTQ0IDE2Ljg4NjUgMzIuMjIzOUMxOS45MzQ1IDM0LjI2NjcgMjMuMjE3IDM1Ljk0NiAyNi40NDkgMzYuNzMyNEMyOS42OTQ2IDM3LjUyMiAzMy4wNDUxIDM3LjQ0MjggMzYuMDAyMSAzNS43MzA5TDMzLjk5NzkgMzIuMjY5MUMzMi4yMDQ5IDMzLjMwNzIgMjkuOTkyOSAzMy40NzggMjcuMzk0NyAzMi44NDU4QzI0Ljc4MyAzMi4yMTAzIDIxLjk0MDUgMzAuNzk1OCAxOS4xMTM1IDI4LjkwMTFDMTMuNDUwOCAyNS4xMDYgOC4yNTY1IDE5LjY2MSA1LjY0ODA4IDE1Ljg2NjlMMi4zNTE5MiAxOC4xMzMxWiIgZmlsbD0iIzMzMyIvPjxwYXRoIGQ9Ik0zMy41OTQ1IDE3QzMyLjgzOTggMTQuNzAyNyAzMC44NTQ5IDkuOTQwNTQgMjcuNTk0NSA3SDExLjU5NDVDMTUuMjE3MSAxMC42NzU3IDIzIDE2IDI3IDI0IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+" alt="feishu">
|
||||
</a>
|
||||
<a href="https://bot.q.qq.com/wiki/">
|
||||
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAC+lBMVEUAAAApRHRAbvYyVI4dMlsNGjcvTH5anN0/a5xdo+Vdpvpms/dMfvdcoPZjr/hJfu5krvA9Z+8/a+M9adxQjNdYmdVAbdU4XtRRjcxFd8wyVbM+aZowUJwpRJlZneRIevU5YORWl+1fqOZVle9ZnOpEdOk/aelepus/a+NQiuVKgeQ6YeRQi905X91EdtxAbthZnNlRjdk5YdpQjuhLgehhquc7Y+c6Y8k3XctWlcs0V8tJfMMzVsM9aMJVksdHebVKgLQyVJknP5REdKD///9irv9Ccf5EdP9GfP9jsf7+/f5SkP9mtf9GeP/+/vxJfv5Ukv9Ng/9boP9Gef1ls/9Ulf9Wl//+//5KgP9Nhv9dpP9Oiv9Tk/5Abv9Lg/9Fdv9NiP7///5Qj/7+/fxfpv9Nh/xhq/9QjP9Wm/79//1co/9ir/5fqf5bov9Qi//8/f1XmP1Wl/1ksv/7/v38/vv9/vlgqv9Znf9IfPzZ5fdeqP9nt/5anv5Xmf9Uk/1anvxpuf73/P34+/tot/9hrf1anvpMhvpSjv1pt/9ZnP1aofxTkfxNivza5/dOhPf+//1Ri/z+//vz+fvw9/tIf/v6/fpdofrI2PlYmfnF3Pe0y/Pm7/jW4/e0zvRapPxXlfpTkvpGevri7ffS3vVMiP5XnvxXnPtQivn3/Ph0r/dgpPZ1qfXi6/S9z/NYmf/z+v5Yn/7L2vlfmflUk/hXi/dRgvSw0fGnu+1dqP3t9Pthq/vA1vjc6vfU4vd4sfdepPdsn/dam/fU5fawzfRjjfRhi/NJhP0/a/y80fhTjfhrrfeXvvZuk/WyyPR9rvSqz/OcwfOmv/OSuPO0yfKguPCWr/BPi+9MheRKgv1gsPzq9PpgpvpVjvrN2/jG1vdonvbA0PSrx/OtxPCkxPDp8PvN4ftnuPuCufloqPiex/a3z/WPuvWQqvR2mfRplvReivSOqvOKufKGufKCrPKBoPJzlfFjr+88Ze6ftux8nOxxq/SGr/CFr/Cvx+3R19+QAAAARHRSTlMAEP4ZEwUEvC3S/v7+/Pv08PDXvJqampqSgEtJLS3f/Pz4+O/n5+bl4tTU1Lq6ure2trGdnZ2dlI6Afl5eXldMS0lJLRAR2gcAAAewSURBVFjDlZcFfBJRHMdvdnd3d3cn1qk3h4Et7EQ965h3KKgTQUGdOgYMHII6a5vO2d3d3d3d3X4+/t974A51xvfDu7vfP348HsdxR6VAulaN6lUu8SHVgQOpPpSoXK9Rq3TUf5CmSdWcB/oFcCBn1SZp/rG9aP6c/X5LzvxF/6W9Vqq2KZKq1t8sgvKT9pQt8gf9qb9QOVLWjux+K8oVSrE9bYNU7f6BVA3SpjD93O3+kdy//Ripcw34QefOnaViwM8iV+rf9Ffo7Kdr166du87vSgQGZGcIJocq/OIQlAv1EbAHDL/0hXzOhFxBP61f7vbJdJ0PFfN1RBB0MIPAUO7Alazvz+l0WY4+Pbthw4az66QG63Do6dEsuh+W9QO+//moVRet071L8phomUzBGPcGBwe3hxdmn5FVyGS0yZP0TodpHzy/kGQBygYjonUvbtpFraDwGUjYZ2JRVNSKK26+0EXjWNnkZciH26OjPy3gOZ5nVVCq+p0BzfK8wC/4FB2MLfL5+4sU7wkER7/dhN6aYxia0zKmvT0l7DUxHE1zHMcoVLJNb6ODUbB4EYpQHddcPHpXK5OpWNHp8CxbtmzmPqnBvpnLAIeTQdPT3j16EUer+yaQpT8i8pqAVi9h4cu1kZGR69bt7y9h/7p1EFz7cmEC+iTiwkgczUKmkC8ECkIs54wiI4j2j5EXQwj9QyCBNgCRIZbIj3ZRYET+nCVkP4TwKqTJHoJTXlbFscZzlv6WXr1CkpGKXhb0PozVynoj1SiTHV3lmvYC1JZnCQzNOG9a1BY1aAlKqbBYLEm8jWYSnpGypmBQTQ0sUp9iVCqr/bl60SK1UqlchIIzYCABB/BCIRR/breqVMwp9SIlUA2u39mVCPUWBlZQHz8DiT591t5euPD2WiUR6rXfiAKhnBHvhXVkrs9AGWX2dFTLGX0AZfxCTiaTHYwChTjl1LL8HZ+YccvJa52nfCL+IHyT3Lb4kVi1pAqOBPqMjPfSYHBmZBQSI6Nm0TTDXicCFCsy9HWfiDqGDPTxIzEFqTqDMVF6BTIYHEXUKhDMrMGEKddstIr2qajQYzT6sIeIrENVCsVc1VuRQeiU0FAYU4gBEkhfY5CBT4TCDJABkZWoCx0xxGB26BSspqyiFSowwAKUSNvoWUQQA1p/NRTLC9R5El+jZ5BBR9Qzffr0VbTICrNiQSC9yskKrN9g+hkoVOljp2N5njrUBdExlhj07UI4qGVZ7ePpREw/KLJa5xxfqu9sBRjMjCXJQ5SvaI3ehg0mELlz94oVu3ei3IQJE2J3eu0rdt/AAvAZoEMwp2L7Iias0aOlAYO+hHmrV89De5KVqAndicEaUhlLremOCJuHDeZ0D+v+V4jBPFK5hjrfA+E3CAvr8RfCwvwGWJ6nLmgQYTF6Gr4cZACCELN6Z+Kbr1/eJO5cHaOBBAyNpkeYZg86kWbGgAAuUBXDw8OXh4fH6BlkoNGgY43mxumkbR6HySiIomBKcHi2JZ2+odHELIdSzR5YbevMmHBMRar2EEyMXgXxOcvD4Viza88mnuNYhmNpDMMwIr9pzy4NJMOXIwMGDHBfbaog3k+LQzNgHscNmTZkSNwut0mgaQH6OK0NtmDG8yb3rrghY8YMiZuD5jozbhoSQwpShaeNAaYt3obWYIt58TQkzKe3JYisKCoUNKegwUFI8J7257agSi8SIAtT6UqPRxjc+De2axocLzYbzObjSVtX8Dyj1TICv2Jr0nEzsHLMmPFxZvS75d2GMSDGl4bbxypyuXy8wXBLUMloe6JBvtJgGB8BjitdiUfmuO/dc885kuhaaRg/fiVUwUi00zKVcAuqoLEKRVGN5UCE4bhJRdPiFrPZJZcQESEPwOUybxFpWmU6bsCZxuiyXnKEfIQ8wjWTZxSs6WRExNwRfiCByn4IOSRPwr+kit/qikChkvjmNc/Qob2HznWd5G1WgXbc2TF36A9GkI0fqLrj4AWrjT/pmju0d++heShEhqy9EUt3CwKso9F75OHSHTuWLp3bW8LcpSj28IjXCCsoCLuv4GjWDBSm5sCBSG6/zMNJDldjh2fBggVbEwdicOXAz1sh5HHwrBbON3bTdpKoCc1kCrg00xMHzICzwanD0VrTkoESlpi06L/dZuMUMoXjSaaNKIgmQMgzFti48corj1aEk88KJwpjXDJWwhIjSysUVi0HJ4bn1ZWNG1EwT/ItTpnhhCVuO8sKCjDgjNuHSwAD9M8lsJzdvcQXKyO502ueaTQwaNDmza/dl50+g9EStsMMwMB52f168+bho4dDKFNzSkKBQX4mH37w6MT69etPHCaacBiHHj04PHnQoNGkrkDgjWb6NslMJrSRhjZPHhQYSp+WCiCoWHJu2LBhl2BIDSASGMoY9MvNdjFfKUA2RCRHpQbFfne7nrHDqA4dOozCG/Qie6mAQURG6P+VoPS4DED7wONAkT4ohUeeAtm6/QPZCqSlUqJF+U6dOnVDA23Q/ldRvsUfH/vyZuv0R7LlDaL+TOsamSdNmjTuBwEic43W1N/JkDfH1EmIqT78xznyZvjXh+9m6XNM/Ikc6Zulof6DdIUb1s1Y6n3m+/czvy+VsW7Dwik9/n8HzjZEy9x05tIAAAAASUVORK5CYII=" alt="QQ频道">
|
||||
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-Bot-lightgrey?style=social&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTIuODIgMTMwLjg5Ij48ZyBkYXRhLW5hbWU9IuWbvuWxgiAyIj48ZyBkYXRhLW5hbWU9IuWbvuWxgiAxIj48cGF0aCBkPSJNNTUuNjMgMTMwLjhjLTcgMC0xMy45LjA4LTIwLjg2IDAtMTkuMTUtLjI1LTMxLjcxLTExLjQtMzQuMjItMzAuMy00LjA3LTMwLjY2IDE0LjkzLTU5LjIgNDQuODMtNjYuNjQgMi0uNTEgNS4yMS0uMzEgNS4yMS0xLjYzIDAtMi4xMy4xNC0yLjEzLjE0LTUuNTcgMC0uODktMS4zLTEuNDYtMi4yMi0yLjMxLTYuNzMtNi4yMy03LjY3LTEzLjQxLTEtMjAuMTggNS40LTUuNTIgMTEuODctNS40IDE3LjgtLjU5IDYuNDkgNS4yNiA2LjMxIDEzLjA4LS44NiAyMS0uNjguNzQtMS43OCAxLjYtMS43OCAyLjY3djQuMjFjMCAxLjM1IDIuMiAxLjYyIDQuNzkgMi4zNSAzMS4wOSA4LjY1IDQ4LjE3IDM0LjEzIDQ1IDY2LjM3LTEuNzYgMTguMTUtMTQuNTYgMzAuMjMtMzIuNyAzMC42My04LjAyLjE5LTE2LjA3LS4wMS0yNC4xMy0uMDF6IiBmaWxsPSIjMDI5OWZlIi8+PHBhdGggZD0iTTMxLjQ2IDExOC4zOGMtMTAuNS0uNjktMTYuOC02Ljg2LTE4LjM4LTE3LjI3LTMtMTkuNDIgMi43OC0zNS44NiAxOC40Ni00Ny44MyAxNC4xNi0xMC44IDI5Ljg3LTEyIDQ1LjM4LTMuMTkgMTcuMjUgOS44NCAyNC41OSAyNS44MSAyNCA0NS4yOS0uNDkgMTUuOS04LjQyIDIzLjE0LTI0LjM4IDIzLjUtNi41OS4xNC0xMy4xOSAwLTE5Ljc5IDAiIGZpbGw9IiNmZWZlZmUiLz48cGF0aCBkPSJNNDYuMDUgNzkuNThjLjA5IDUgLjIzIDkuODItNyA5Ljc3LTcuODItLjA2LTYuMS01LjY5LTYuMjQtMTAuMTktLjE1LTQuODItLjczLTEwIDYuNzMtOS44NHM2LjM3IDUuNTUgNi41MSAxMC4yNnoiIGZpbGw9IiMxMDlmZmUiLz48cGF0aCBkPSJNODAuMjcgNzkuMjdjLS41MyAzLjkxIDEuNzUgOS42NC01Ljg4IDEwLTcuNDcuMzctNi44MS00LjgyLTYuNjEtOS41LjItNC4zMi0xLjgzLTEwIDUuNzgtMTAuNDJzNi41OSA0Ljg5IDYuNzEgOS45MnoiIGZpbGw9IiMwODljZmUiLz48L2c+PC9nPjwvc3ZnPg==" alt="QQ频道">
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://jq.qq.com/?_wv=1027&k=5OFifDh">
|
||||
<img src="https://img.shields.io/badge/qq%E7%BE%A4-768887710-orange?style=flat-square" alt="QQ Chat">
|
||||
<img src="https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=flat-square" alt="QQ Chat Group">
|
||||
</a>
|
||||
<a href="https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka">
|
||||
<img src="https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-NoneBot-5492ff?style=flat-square" alt="QQ Channel">
|
||||
</a>
|
||||
<a href="https://t.me/botuniverse">
|
||||
<img src="https://img.shields.io/badge/telegram-botuniverse-blue?style=flat-square" alt="Telegram Channel">
|
||||
@@ -180,5 +186,5 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
感谢以下开发者对 NoneBot2 作出的贡献:
|
||||
|
||||
<a href="https://github.com/nonebot/nonebot2/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=nonebot/nonebot2" />
|
||||
<img src="https://contrib.rocks/image?repo=nonebot/nonebot2&max=1000" />
|
||||
</a>
|
||||
|
@@ -11,6 +11,7 @@
|
||||
- `on_request` => {ref}``on_request` <nonebot.plugin.on.on_request>`
|
||||
- `on_startswith` => {ref}``on_startswith` <nonebot.plugin.on.on_startswith>`
|
||||
- `on_endswith` => {ref}``on_endswith` <nonebot.plugin.on.on_endswith>`
|
||||
- `on_fullmatch` => {ref}``on_fullmatch` <nonebot.plugin.on.on_fullmatch>`
|
||||
- `on_keyword` => {ref}``on_keyword` <nonebot.plugin.on.on_keyword>`
|
||||
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
|
||||
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
|
||||
@@ -24,8 +25,10 @@
|
||||
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
|
||||
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
|
||||
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
|
||||
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.plugin.get_plugin>`
|
||||
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.plugin.get_loaded_plugins>`
|
||||
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.get_plugin>`
|
||||
- `get_plugin_by_module_name` => {ref}``get_plugin_by_module_name` <nonebot.plugin.get_plugin_by_module_name>`
|
||||
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.get_loaded_plugins>`
|
||||
- `get_available_plugin_names` => {ref}``get_available_plugin_names` <nonebot.plugin.get_available_plugin_names>`
|
||||
- `export` => {ref}``export` <nonebot.plugin.export.export>`
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
|
||||
@@ -37,8 +40,6 @@ FrontMatter:
|
||||
import importlib
|
||||
from typing import Any, Dict, Type, Optional
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.utils import escape_tag
|
||||
from nonebot.config import Env, Config
|
||||
@@ -46,10 +47,12 @@ from nonebot.log import logger, default_filter
|
||||
from nonebot.drivers import Driver, ReverseDriver, combine_driver
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
|
||||
_dist: pkg_resources.Distribution = pkg_resources.get_distribution("nonebot2")
|
||||
__version__ = _dist.version
|
||||
VERSION = _dist.parsed_version
|
||||
except pkg_resources.DistributionNotFound: # pragma: no cover
|
||||
except Exception: # pragma: no cover
|
||||
__version__ = None
|
||||
VERSION = None
|
||||
|
||||
@@ -273,6 +276,7 @@ from nonebot.plugin import on_endswith as on_endswith
|
||||
from nonebot.plugin import CommandGroup as CommandGroup
|
||||
from nonebot.plugin import MatcherGroup as MatcherGroup
|
||||
from nonebot.plugin import load_plugins as load_plugins
|
||||
from nonebot.plugin import on_fullmatch as on_fullmatch
|
||||
from nonebot.plugin import on_metaevent as on_metaevent
|
||||
from nonebot.plugin import on_startswith as on_startswith
|
||||
from nonebot.plugin import load_from_json as load_from_json
|
||||
@@ -282,5 +286,7 @@ from nonebot.plugin import on_shell_command as on_shell_command
|
||||
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
|
||||
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin
|
||||
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
|
||||
from nonebot.plugin import get_plugin_by_module_name as get_plugin_by_module_name
|
||||
from nonebot.plugin import get_available_plugin_names as get_available_plugin_names
|
||||
|
||||
__autodoc__ = {"internal": False}
|
||||
|
@@ -56,7 +56,7 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
if env_path.is_file():
|
||||
env_file_vars = read_env_file(
|
||||
env_path,
|
||||
encoding=env_file_encoding,
|
||||
encoding=env_file_encoding, # type: ignore
|
||||
case_sensitive=settings.__config__.case_sensitive,
|
||||
)
|
||||
env_vars = {**env_file_vars, **env_vars}
|
||||
@@ -83,9 +83,8 @@ class CustomEnvSettings(EnvSettingsSource):
|
||||
d[field.alias] = env_val
|
||||
|
||||
if env_file_vars:
|
||||
for env_name, env_val in env_file_vars.items():
|
||||
if (env_val is None or len(env_val) == 0) and env_name in env_vars:
|
||||
env_val = env_vars[env_name]
|
||||
for env_name in env_file_vars.keys():
|
||||
env_val = env_vars[env_name]
|
||||
try:
|
||||
if env_val:
|
||||
env_val = settings.__config__.json_loads(env_val.strip())
|
||||
|
@@ -28,6 +28,8 @@ RAW_CMD_KEY: Literal["raw_command"] = "raw_command"
|
||||
"""命令文本存储 key"""
|
||||
CMD_ARG_KEY: Literal["command_arg"] = "command_arg"
|
||||
"""命令参数存储 key"""
|
||||
CMD_START_KEY: Literal["command_start"] = "command_start"
|
||||
"""命令开头存储 key"""
|
||||
|
||||
SHELL_ARGS: Literal["_args"] = "_args"
|
||||
"""shell 命令 parse 后参数字典存储 key"""
|
||||
|
@@ -194,7 +194,7 @@ class Dependent(Generic[R]):
|
||||
field_info = field.field_info
|
||||
assert isinstance(field_info, Param), "Params must be subclasses of Param"
|
||||
value = await field_info._solve(**params)
|
||||
if value == Undefined:
|
||||
if value is Undefined:
|
||||
value = field.get_default()
|
||||
|
||||
try:
|
||||
|
@@ -1,15 +1,16 @@
|
||||
import signal
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Set, Callable, Awaitable
|
||||
from typing import Set, Union, Callable, Awaitable
|
||||
|
||||
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
|
||||
|
||||
STARTUP_FUNC = Callable[[], Awaitable[None]]
|
||||
SHUTDOWN_FUNC = Callable[[], Awaitable[None]]
|
||||
STARTUP_FUNC = Callable[[], Union[None, Awaitable[None]]]
|
||||
SHUTDOWN_FUNC = Callable[[], Union[None, Awaitable[None]]]
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
||||
@@ -69,7 +70,10 @@ class BlockDriver(Driver):
|
||||
|
||||
async def startup(self):
|
||||
# run startup
|
||||
cors = [startup() for startup in self.startup_funcs]
|
||||
cors = [
|
||||
startup() if is_coroutine_callable(startup) else run_sync(startup)()
|
||||
for startup in self.startup_funcs
|
||||
]
|
||||
if cors:
|
||||
try:
|
||||
await asyncio.gather(*cors)
|
||||
@@ -89,7 +93,10 @@ class BlockDriver(Driver):
|
||||
|
||||
logger.info("Waiting for application shutdown.")
|
||||
# run shutdown
|
||||
cors = [shutdown() for shutdown in self.shutdown_funcs]
|
||||
cors = [
|
||||
shutdown() if is_coroutine_callable(shutdown) else run_sync(shutdown)()
|
||||
for shutdown in self.shutdown_funcs
|
||||
]
|
||||
if cors:
|
||||
try:
|
||||
await asyncio.gather(*cors)
|
||||
|
@@ -132,20 +132,33 @@ class WebSocket(BaseWebSocket):
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def receive(self) -> str:
|
||||
msg = await self._receive()
|
||||
if msg.type not in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY):
|
||||
raise TypeError(
|
||||
f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}"
|
||||
)
|
||||
return msg.data
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def receive_text(self) -> str:
|
||||
msg = await self._receive()
|
||||
if msg.type != aiohttp.WSMsgType.TEXT:
|
||||
raise TypeError(f"WebSocket received unexpected frame type: {msg.type}")
|
||||
raise TypeError(
|
||||
f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}"
|
||||
)
|
||||
return msg.data
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def receive_bytes(self) -> bytes:
|
||||
msg = await self._receive()
|
||||
if msg.type != aiohttp.WSMsgType.TEXT:
|
||||
raise TypeError(f"WebSocket received unexpected frame type: {msg.type}")
|
||||
if msg.type != aiohttp.WSMsgType.BINARY:
|
||||
raise TypeError(
|
||||
f"WebSocket received unexpected frame type: {msg.type}, {msg.data!r}"
|
||||
)
|
||||
return msg.data
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def send(self, data: str) -> None:
|
||||
async def send_text(self, data: str) -> None:
|
||||
await self.websocket.send_str(data)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
|
@@ -11,7 +11,7 @@ FrontMatter:
|
||||
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Any, List, Tuple, Callable, Optional
|
||||
from typing import Any, List, Tuple, Union, Callable, Optional
|
||||
|
||||
import uvicorn
|
||||
from pydantic import BaseSettings
|
||||
@@ -36,6 +36,8 @@ def catch_closed(func):
|
||||
return await func(*args, **kwargs)
|
||||
except WebSocketDisconnect as e:
|
||||
raise WebSocketClosed(e.code)
|
||||
except KeyError:
|
||||
raise TypeError("WebSocket received unexpected frame type")
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -55,7 +57,7 @@ class Config(BaseSettings):
|
||||
"""开启/关闭冷重载"""
|
||||
fastapi_reload_dirs: Optional[List[str]] = None
|
||||
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
||||
fastapi_reload_delay: Optional[float] = None
|
||||
fastapi_reload_delay: float = 0.25
|
||||
"""重载延迟,默认为 uvicorn 默认值"""
|
||||
fastapi_reload_includes: Optional[List[str]] = None
|
||||
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
@@ -261,9 +263,17 @@ class FastAPIWebSocket(BaseWebSocket):
|
||||
) -> None:
|
||||
await self.websocket.close(code)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
# assert self.websocket.application_state == WebSocketState.CONNECTED
|
||||
msg = await self.websocket.receive()
|
||||
if msg["type"] == "websocket.disconnect":
|
||||
raise WebSocketClosed(msg["code"])
|
||||
return msg["text"] if "text" in msg else msg["bytes"]
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
async def receive(self) -> str:
|
||||
async def receive_text(self) -> str:
|
||||
return await self.websocket.receive_text()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@@ -272,7 +282,7 @@ class FastAPIWebSocket(BaseWebSocket):
|
||||
return await self.websocket.receive_bytes()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def send(self, data: str) -> None:
|
||||
async def send_text(self, data: str) -> None:
|
||||
await self.websocket.send({"type": "websocket.send", "text": data})
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
|
@@ -49,22 +49,22 @@ class Mixin(ForwardMixin):
|
||||
async def request(self, setup: Request) -> Response:
|
||||
async with httpx.AsyncClient(
|
||||
http2=setup.version == HTTPVersion.H2,
|
||||
proxies=setup.proxy,
|
||||
proxies=setup.proxy, # type: ignore
|
||||
follow_redirects=True,
|
||||
) as client:
|
||||
response = await client.request(
|
||||
setup.method,
|
||||
str(setup.url),
|
||||
content=setup.content,
|
||||
data=setup.data,
|
||||
content=setup.content, # type: ignore
|
||||
data=setup.data, # type: ignore
|
||||
json=setup.json,
|
||||
files=setup.files,
|
||||
files=setup.files, # type: ignore
|
||||
headers=tuple(setup.headers.items()),
|
||||
timeout=setup.timeout,
|
||||
)
|
||||
return Response(
|
||||
response.status_code,
|
||||
headers=response.headers,
|
||||
headers=response.headers.multi_items(),
|
||||
content=response.content,
|
||||
request=setup,
|
||||
)
|
||||
|
@@ -17,7 +17,7 @@ FrontMatter:
|
||||
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from typing import List, Tuple, TypeVar, Callable, Optional, Coroutine
|
||||
from typing import List, Tuple, Union, TypeVar, Callable, Optional, Coroutine
|
||||
|
||||
import uvicorn
|
||||
from pydantic import BaseSettings
|
||||
@@ -38,7 +38,7 @@ try:
|
||||
from quart.datastructures import FileStorage
|
||||
from quart import Websocket as QuartWebSocket
|
||||
except ImportError:
|
||||
raise ValueError(
|
||||
raise ImportError(
|
||||
"Please install Quart by using `pip install nonebot2[quart]`"
|
||||
) from None
|
||||
|
||||
@@ -63,7 +63,7 @@ class Config(BaseSettings):
|
||||
"""开启/关闭冷重载"""
|
||||
quart_reload_dirs: Optional[List[str]] = None
|
||||
"""重载监控文件夹列表,默认为 uvicorn 默认值"""
|
||||
quart_reload_delay: Optional[float] = None
|
||||
quart_reload_delay: float = 0.25
|
||||
"""重载延迟,默认为 uvicorn 默认值"""
|
||||
quart_reload_includes: Optional[List[str]] = None
|
||||
"""要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值"""
|
||||
@@ -199,7 +199,7 @@ class Driver(ReverseDriver):
|
||||
http_request = BaseRequest(
|
||||
request.method,
|
||||
request.url,
|
||||
headers=request.headers.items(),
|
||||
headers=list(request.headers.items()),
|
||||
cookies=list(request.cookies.items()),
|
||||
content=await request.get_data(
|
||||
cache=False, as_text=False, parse_form_data=False
|
||||
@@ -224,7 +224,7 @@ class Driver(ReverseDriver):
|
||||
http_request = BaseRequest(
|
||||
websocket.method,
|
||||
websocket.url,
|
||||
headers=websocket.headers.items(),
|
||||
headers=list(websocket.headers.items()),
|
||||
cookies=list(websocket.cookies.items()),
|
||||
version=websocket.http_version,
|
||||
)
|
||||
@@ -257,7 +257,12 @@ class WebSocket(BaseWebSocket):
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
async def receive(self) -> str:
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
return await self.websocket.receive()
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
async def receive_text(self) -> str:
|
||||
msg = await self.websocket.receive()
|
||||
if isinstance(msg, bytes):
|
||||
raise TypeError("WebSocket received unexpected frame type: bytes")
|
||||
@@ -272,7 +277,7 @@ class WebSocket(BaseWebSocket):
|
||||
return msg
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def send(self, data: str):
|
||||
async def send_text(self, data: str):
|
||||
await self.websocket.send(data)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
|
@@ -16,8 +16,8 @@ FrontMatter:
|
||||
"""
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Type, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Type, Union, AsyncGenerator
|
||||
|
||||
from nonebot.typing import overrides
|
||||
from nonebot.log import LoguruHandler
|
||||
@@ -46,9 +46,9 @@ def catch_closed(func):
|
||||
return await func(*args, **kwargs)
|
||||
except ConnectionClosed as e:
|
||||
if e.rcvd_then_sent:
|
||||
raise WebSocketClosed(e.rcvd.code, e.rcvd.reason)
|
||||
raise WebSocketClosed(e.rcvd.code, e.rcvd.reason) # type: ignore
|
||||
else:
|
||||
raise WebSocketClosed(e.sent.code, e.sent.reason)
|
||||
raise WebSocketClosed(e.sent.code, e.sent.reason) # type: ignore
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -100,7 +100,13 @@ class WebSocket(BaseWebSocket):
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
async def receive(self) -> str:
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
msg = await self.websocket.recv()
|
||||
return msg
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
@catch_closed
|
||||
async def receive_text(self) -> str:
|
||||
msg = await self.websocket.recv()
|
||||
if isinstance(msg, bytes):
|
||||
raise TypeError("WebSocket received unexpected frame type: bytes")
|
||||
@@ -115,7 +121,7 @@ class WebSocket(BaseWebSocket):
|
||||
return msg
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
async def send(self, data: str) -> None:
|
||||
async def send_text(self, data: str) -> None:
|
||||
await self.websocket.send(data)
|
||||
|
||||
@overrides(BaseWebSocket)
|
||||
|
@@ -7,11 +7,11 @@ NoneBotException
|
||||
├── ParserExit
|
||||
├── ProcessException
|
||||
| ├── IgnoredException
|
||||
| ├── SkippedException
|
||||
| | └── TypeMisMatch
|
||||
| ├── MockApiException
|
||||
| └── StopPropagation
|
||||
├── MatcherException
|
||||
| ├── SkippedException
|
||||
| | └── TypeMisMatch
|
||||
| ├── PausedException
|
||||
| ├── RejectedException
|
||||
| └── FinishedException
|
||||
@@ -75,6 +75,37 @@ class IgnoredException(ProcessException):
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class SkippedException(ProcessException):
|
||||
"""指示 NoneBot 立即结束当前 `Dependent` 的运行。
|
||||
|
||||
例如,可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.skip` 抛出。
|
||||
|
||||
用法:
|
||||
```python
|
||||
def always_skip():
|
||||
Matcher.skip()
|
||||
|
||||
@matcher.handle()
|
||||
async def handler(dependency = Depends(always_skip)):
|
||||
# never run
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
class TypeMisMatch(SkippedException):
|
||||
"""当前 `Handler` 的参数类型不匹配。"""
|
||||
|
||||
def __init__(self, param: ModelField, value: Any):
|
||||
self.param: ModelField = param
|
||||
self.value: Any = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TypeMisMatch, param={self.param}, value={self.value}>"
|
||||
|
||||
def __str__(self):
|
||||
self.__repr__()
|
||||
|
||||
|
||||
class MockApiException(ProcessException):
|
||||
"""指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。可由 api hook 抛出。
|
||||
|
||||
@@ -114,37 +145,6 @@ class MatcherException(NoneBotException):
|
||||
"""所有 Matcher 发生的异常基类。"""
|
||||
|
||||
|
||||
class SkippedException(MatcherException):
|
||||
"""指示 NoneBot 立即结束当前 `Handler` 的处理,继续处理下一个 `Handler`。
|
||||
|
||||
可以在 `Handler` 中通过 {ref}`nonebot.matcher.Matcher.skip` 抛出。
|
||||
|
||||
用法:
|
||||
```python
|
||||
def always_skip():
|
||||
Matcher.skip()
|
||||
|
||||
@matcher.handle()
|
||||
async def handler(dependency = Depends(always_skip)):
|
||||
...
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
class TypeMisMatch(SkippedException):
|
||||
"""当前 `Handler` 的参数类型不匹配。"""
|
||||
|
||||
def __init__(self, param: ModelField, value: Any):
|
||||
self.param: ModelField = param
|
||||
self.value: Any = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TypeMisMatch, param={self.param}, value={self.value}>"
|
||||
|
||||
def __str__(self):
|
||||
self.__repr__()
|
||||
|
||||
|
||||
class PausedException(MatcherException):
|
||||
"""指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。可用于用户输入新信息。
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import abc
|
||||
from typing import Any, Type, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -6,6 +7,8 @@ from nonebot.utils import DataclassEncoder
|
||||
|
||||
from .message import Message
|
||||
|
||||
E = TypeVar("E", bound="Event")
|
||||
|
||||
|
||||
class Event(abc.ABC, BaseModel):
|
||||
"""Event 基类。提供获取关键信息的方法,其余信息可直接获取。"""
|
||||
@@ -14,6 +17,12 @@ class Event(abc.ABC, BaseModel):
|
||||
extra = "allow"
|
||||
json_encoders = {Message: DataclassEncoder}
|
||||
|
||||
@classmethod
|
||||
def validate(cls: Type["E"], value: Any) -> "E":
|
||||
if isinstance(value, Event) and not isinstance(value, cls):
|
||||
raise TypeError(f"{value} is incompatible with Event type {cls}")
|
||||
return super().validate(value)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_type(self) -> str:
|
||||
"""获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。"""
|
||||
|
@@ -66,7 +66,11 @@ class MessageSegment(abc.ABC, Generic[TM]):
|
||||
return value
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"Expected dict for MessageSegment, got {type(value)}")
|
||||
return cls(**value)
|
||||
if "type" not in value:
|
||||
raise ValueError(
|
||||
f"Expected dict with 'type' for MessageSegment, got {value}"
|
||||
)
|
||||
return cls(type=value["type"], data=value.get("data", {}))
|
||||
|
||||
def get(self, key: str, default: Any = None):
|
||||
return asdict(self).get(key, default)
|
||||
|
@@ -49,7 +49,9 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def __init__(self, template, factory=str) -> None:
|
||||
def __init__( # type:ignore
|
||||
self, template, factory=str
|
||||
) -> None: # TODO: fix type hint here
|
||||
self.template: TF = template
|
||||
self.factory: Type[TF] = factory
|
||||
self.format_specs: Dict[str, FormatSpecFunc] = {}
|
||||
@@ -72,25 +74,37 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
return self._format([], mapping)
|
||||
|
||||
def _format(self, args: Sequence[Any], kwargs: Mapping[str, Any]) -> TF:
|
||||
msg = self.factory()
|
||||
full_message = self.factory()
|
||||
used_args, arg_index = set(), 0
|
||||
|
||||
if isinstance(self.template, str):
|
||||
msg += self.vformat(self.template, args, kwargs)
|
||||
msg, arg_index = self._vformat(
|
||||
self.template, args, kwargs, used_args, arg_index
|
||||
)
|
||||
full_message += msg
|
||||
elif isinstance(self.template, self.factory):
|
||||
template = cast("Message[MessageSegment]", self.template)
|
||||
for seg in template:
|
||||
msg += self.vformat(str(seg), args, kwargs) if seg.is_text() else seg
|
||||
if not seg.is_text():
|
||||
full_message += seg
|
||||
else:
|
||||
msg, arg_index = self._vformat(
|
||||
str(seg), args, kwargs, used_args, arg_index
|
||||
)
|
||||
full_message += msg
|
||||
else:
|
||||
raise TypeError("template must be a string or instance of Message!")
|
||||
|
||||
return msg # type:ignore
|
||||
self.check_unused_args(list(used_args), args, kwargs)
|
||||
return cast(TF, full_message)
|
||||
|
||||
def vformat(
|
||||
self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]
|
||||
self,
|
||||
format_string: str,
|
||||
args: Sequence[Any],
|
||||
kwargs: Mapping[str, Any],
|
||||
) -> TF:
|
||||
used_args = set()
|
||||
result, _ = self._vformat(format_string, args, kwargs, used_args, 2)
|
||||
self.check_unused_args(list(used_args), args, kwargs)
|
||||
return result
|
||||
raise NotImplementedError("`vformat` has merged into `_format`")
|
||||
|
||||
def _vformat(
|
||||
self,
|
||||
@@ -98,12 +112,8 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
args: Sequence[Any],
|
||||
kwargs: Mapping[str, Any],
|
||||
used_args: Set[Union[int, str]],
|
||||
recursion_depth: int,
|
||||
auto_arg_index: int = 0,
|
||||
) -> Tuple[TF, int]:
|
||||
if recursion_depth < 0:
|
||||
raise ValueError("Max string recursion exceeded")
|
||||
|
||||
results: List[Any] = [self.factory()]
|
||||
|
||||
for (literal_text, field_name, format_spec, conversion) in self.parse(
|
||||
@@ -143,23 +153,13 @@ class MessageTemplate(Formatter, Generic[TF]):
|
||||
obj, arg_used = self.get_field(field_name, args, kwargs)
|
||||
used_args.add(arg_used)
|
||||
|
||||
assert format_spec is not None
|
||||
|
||||
# do any conversion on the resulting object
|
||||
obj = self.convert_field(obj, conversion) if conversion else obj
|
||||
|
||||
# expand the format spec, if needed
|
||||
format_control, auto_arg_index = self._vformat(
|
||||
format_spec,
|
||||
args,
|
||||
kwargs,
|
||||
used_args,
|
||||
recursion_depth - 1,
|
||||
auto_arg_index,
|
||||
)
|
||||
|
||||
# format the object and append to the result
|
||||
formatted_text = self.format_field(obj, str(format_control))
|
||||
formatted_text = (
|
||||
self.format_field(obj, format_spec) if format_spec else obj
|
||||
)
|
||||
results.append(formatted_text)
|
||||
|
||||
return functools.reduce(self._add, results), auto_arg_index
|
||||
|
@@ -4,9 +4,10 @@ from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag
|
||||
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
|
||||
|
||||
@@ -128,7 +129,12 @@ class Driver(abc.ABC):
|
||||
self._clients[bot.self_id] = bot
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
coros = list(map(lambda x: x(bot=bot), self._bot_connection_hook))
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
|
||||
self._bot_connection_hook,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
@@ -146,7 +152,12 @@ class Driver(abc.ABC):
|
||||
del self._clients[bot.self_id]
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
coros = list(map(lambda x: x(bot=bot), self._bot_disconnection_hook))
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: run_coro_with_catch(x(bot=bot), (SkippedException,)),
|
||||
self._bot_disconnection_hook,
|
||||
)
|
||||
)
|
||||
if coros:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
|
@@ -186,7 +186,12 @@ class WebSocket(abc.ABC):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
async def receive(self) -> str:
|
||||
async def receive(self) -> Union[str, bytes]:
|
||||
"""接收一条 WebSocket text/bytes 信息"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
async def receive_text(self) -> str:
|
||||
"""接收一条 WebSocket text 信息"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -195,8 +200,17 @@ class WebSocket(abc.ABC):
|
||||
"""接收一条 WebSocket binary 信息"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def send(self, data: Union[str, bytes]) -> None:
|
||||
"""发送一条 WebSocket text/bytes 信息"""
|
||||
if isinstance(data, str):
|
||||
await self.send_text(data)
|
||||
elif isinstance(data, bytes):
|
||||
await self.send_bytes(data)
|
||||
else:
|
||||
raise TypeError("WebSocker send method expects str or bytes!")
|
||||
|
||||
@abc.abstractmethod
|
||||
async def send(self, data: str) -> None:
|
||||
async def send_text(self, data: str) -> None:
|
||||
"""发送一条 WebSocket text 信息"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -248,8 +262,8 @@ class Cookies(MutableMapping):
|
||||
self,
|
||||
name: str,
|
||||
default: Optional[str] = None,
|
||||
domain: str = None,
|
||||
path: str = None,
|
||||
domain: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
value: Optional[str] = None
|
||||
for cookie in self.jar:
|
||||
|
@@ -1,8 +1,8 @@
|
||||
from types import ModuleType
|
||||
from datetime import datetime
|
||||
from contextvars import ContextVar
|
||||
from collections import defaultdict
|
||||
from contextlib import AsyncExitStack
|
||||
from datetime import datetime, timedelta
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -168,7 +168,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
*,
|
||||
plugin: Optional["Plugin"] = None,
|
||||
module: Optional[ModuleType] = None,
|
||||
expire_time: Optional[datetime] = None,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
default_state: Optional[T_State] = None,
|
||||
default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None,
|
||||
default_permission_updater: Optional[
|
||||
@@ -216,7 +216,11 @@ class Matcher(metaclass=MatcherMeta):
|
||||
if handlers
|
||||
else [],
|
||||
"temp": temp,
|
||||
"expire_time": expire_time,
|
||||
"expire_time": (
|
||||
expire_time
|
||||
if isinstance(expire_time, datetime)
|
||||
else expire_time and datetime.now() + expire_time
|
||||
),
|
||||
"priority": priority,
|
||||
"block": block,
|
||||
"_default_state": default_state or {},
|
||||
@@ -682,7 +686,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
block=True,
|
||||
plugin=self.plugin,
|
||||
module=self.module,
|
||||
expire_time=datetime.now() + bot.config.session_expire_timeout,
|
||||
expire_time=bot.config.session_expire_timeout,
|
||||
default_state=self.state,
|
||||
default_type_updater=self.__class__._default_type_updater,
|
||||
default_permission_updater=self.__class__._default_permission_updater,
|
||||
@@ -701,7 +705,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
block=True,
|
||||
plugin=self.plugin,
|
||||
module=self.module,
|
||||
expire_time=datetime.now() + bot.config.session_expire_timeout,
|
||||
expire_time=bot.config.session_expire_timeout,
|
||||
default_state=self.state,
|
||||
default_type_updater=self.__class__._default_type_updater,
|
||||
default_permission_updater=self.__class__._default_permission_updater,
|
||||
|
@@ -3,6 +3,7 @@ from contextlib import AsyncExitStack
|
||||
from typing import Any, Set, Tuple, Union, NoReturn, Optional, Coroutine
|
||||
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.utils import run_coro_with_catch
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.typing import T_DependencyCache, T_PermissionChecker
|
||||
|
||||
@@ -10,13 +11,6 @@ from .adapter import Bot, Event
|
||||
from .params import BotParam, EventParam, DependParam, DefaultParam
|
||||
|
||||
|
||||
async def _run_coro_with_catch(coro: Coroutine[Any, Any, Any]):
|
||||
try:
|
||||
return await coro
|
||||
except SkippedException:
|
||||
return False
|
||||
|
||||
|
||||
class Permission:
|
||||
"""{ref}`nonebot.matcher.Matcher` 权限类。
|
||||
|
||||
@@ -72,20 +66,22 @@ class Permission:
|
||||
return True
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
_run_coro_with_catch(
|
||||
run_coro_with_catch(
|
||||
checker(
|
||||
bot=bot,
|
||||
event=event,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
),
|
||||
(SkippedException,),
|
||||
False,
|
||||
)
|
||||
for checker in self.checkers
|
||||
),
|
||||
)
|
||||
return any(results)
|
||||
|
||||
def __and__(self, other) -> NoReturn:
|
||||
def __and__(self, other: object) -> NoReturn:
|
||||
raise RuntimeError("And operation between Permissions is not allowed.")
|
||||
|
||||
def __or__(
|
||||
@@ -98,6 +94,16 @@ class Permission:
|
||||
else:
|
||||
return Permission(*self.checkers, other)
|
||||
|
||||
def __ror__(
|
||||
self, other: Optional[Union["Permission", T_PermissionChecker]]
|
||||
) -> "Permission":
|
||||
if other is None:
|
||||
return self
|
||||
elif isinstance(other, Permission):
|
||||
return Permission(*other.checkers, *self.checkers)
|
||||
else:
|
||||
return Permission(other, *self.checkers)
|
||||
|
||||
|
||||
class User:
|
||||
"""检查当前事件是否属于指定会话
|
||||
|
@@ -91,5 +91,13 @@ class Rule:
|
||||
else:
|
||||
return Rule(*self.checkers, other)
|
||||
|
||||
def __or__(self, other) -> NoReturn:
|
||||
def __rand__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
|
||||
if other is None:
|
||||
return self
|
||||
elif isinstance(other, Rule):
|
||||
return Rule(*other.checkers, *self.checkers)
|
||||
else:
|
||||
return Rule(other, *self.checkers)
|
||||
|
||||
def __or__(self, other: object) -> NoReturn:
|
||||
raise RuntimeError("Or operation between rules is not allowed.")
|
||||
|
@@ -96,7 +96,6 @@ default_format: str = (
|
||||
logger_id = logger.add(
|
||||
sys.stdout,
|
||||
level=0,
|
||||
colorize=True,
|
||||
diagnose=False,
|
||||
filter=default_filter,
|
||||
format=default_format,
|
||||
|
@@ -14,9 +14,9 @@ from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional, Coroutine
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.rule import TrieRule
|
||||
from nonebot.utils import escape_tag
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from nonebot.utils import escape_tag, run_coro_with_catch
|
||||
from nonebot.exception import (
|
||||
NoLogException,
|
||||
StopPropagation,
|
||||
@@ -110,13 +110,6 @@ def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
|
||||
return func
|
||||
|
||||
|
||||
async def _run_coro_with_catch(coro: Coroutine[Any, Any, Any]) -> Any:
|
||||
try:
|
||||
return await coro
|
||||
except SkippedException:
|
||||
pass
|
||||
|
||||
|
||||
async def _check_matcher(
|
||||
priority: int,
|
||||
Matcher: Type[Matcher],
|
||||
@@ -167,7 +160,7 @@ async def _run_matcher(
|
||||
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: _run_coro_with_catch(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
matcher=matcher,
|
||||
bot=bot,
|
||||
@@ -175,7 +168,8 @@ async def _run_matcher(
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
),
|
||||
(SkippedException,),
|
||||
),
|
||||
_run_preprocessors,
|
||||
)
|
||||
@@ -208,16 +202,17 @@ async def _run_matcher(
|
||||
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: _run_coro_with_catch(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
matcher=matcher,
|
||||
exception=exception,
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
state=matcher.state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
),
|
||||
(SkippedException,),
|
||||
),
|
||||
_run_postprocessors,
|
||||
)
|
||||
@@ -263,14 +258,15 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
async with AsyncExitStack() as stack:
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: _run_coro_with_catch(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
),
|
||||
(SkippedException,),
|
||||
),
|
||||
_event_preprocessors,
|
||||
)
|
||||
@@ -330,14 +326,15 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: _run_coro_with_catch(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
),
|
||||
(SkippedException,),
|
||||
),
|
||||
_event_postprocessors,
|
||||
)
|
||||
|
@@ -32,6 +32,7 @@ from nonebot.consts import (
|
||||
CMD_ARG_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
CMD_START_KEY,
|
||||
REGEX_MATCHED,
|
||||
)
|
||||
|
||||
@@ -99,6 +100,15 @@ def CommandArg() -> Any:
|
||||
return Depends(_command_arg)
|
||||
|
||||
|
||||
def _command_start(state: T_State) -> str:
|
||||
return state[PREFIX_KEY][CMD_START_KEY]
|
||||
|
||||
|
||||
def CommandStart() -> str:
|
||||
"""消息命令开头"""
|
||||
return Depends(_command_start)
|
||||
|
||||
|
||||
def _shell_command_args(state: T_State) -> Any:
|
||||
return state[SHELL_ARGS]
|
||||
|
||||
|
@@ -11,6 +11,7 @@
|
||||
- `on_request` => {ref}``on_request` <nonebot.plugin.on.on_request>`
|
||||
- `on_startswith` => {ref}``on_startswith` <nonebot.plugin.on.on_startswith>`
|
||||
- `on_endswith` => {ref}``on_endswith` <nonebot.plugin.on.on_endswith>`
|
||||
- `on_fullmatch` => {ref}``on_fullmatch` <nonebot.plugin.on.on_fullmatch>`
|
||||
- `on_keyword` => {ref}``on_keyword` <nonebot.plugin.on.on_keyword>`
|
||||
- `on_command` => {ref}``on_command` <nonebot.plugin.on.on_command>`
|
||||
- `on_shell_command` => {ref}``on_shell_command` <nonebot.plugin.on.on_shell_command>`
|
||||
@@ -24,8 +25,6 @@
|
||||
- `load_from_toml` => {ref}``load_from_toml` <nonebot.plugin.load.load_from_toml>`
|
||||
- `load_builtin_plugin` => {ref}``load_builtin_plugin` <nonebot.plugin.load.load_builtin_plugin>`
|
||||
- `load_builtin_plugins` => {ref}``load_builtin_plugins` <nonebot.plugin.load.load_builtin_plugins>`
|
||||
- `get_plugin` => {ref}``get_plugin` <nonebot.plugin.plugin.get_plugin>`
|
||||
- `get_loaded_plugins` => {ref}``get_loaded_plugins` <nonebot.plugin.plugin.get_loaded_plugins>`
|
||||
- `export` => {ref}``export` <nonebot.plugin.export.export>`
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
|
||||
@@ -34,14 +33,77 @@ FrontMatter:
|
||||
description: nonebot.plugin 模块
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from itertools import chain
|
||||
from types import ModuleType
|
||||
from contextvars import ContextVar
|
||||
from typing import Set, Dict, List, Tuple, Optional
|
||||
|
||||
_plugins: Dict[str, "Plugin"] = {}
|
||||
_managers: List["PluginManager"] = []
|
||||
_current_plugin: ContextVar[Optional["Plugin"]] = ContextVar(
|
||||
"_current_plugin", default=None
|
||||
_current_plugin_chain: ContextVar[Tuple["Plugin", ...]] = ContextVar(
|
||||
"_current_plugin_chain", default=tuple()
|
||||
)
|
||||
|
||||
|
||||
def _module_name_to_plugin_name(module_name: str) -> str:
|
||||
return module_name.rsplit(".", 1)[-1]
|
||||
|
||||
|
||||
def _new_plugin(
|
||||
module_name: str, module: ModuleType, manager: "PluginManager"
|
||||
) -> "Plugin":
|
||||
plugin_name = _module_name_to_plugin_name(module_name)
|
||||
if plugin_name in _plugins:
|
||||
raise RuntimeError("Plugin already exists! Check your plugin name.")
|
||||
plugin = Plugin(plugin_name, module, module_name, manager)
|
||||
_plugins[plugin_name] = plugin
|
||||
return plugin
|
||||
|
||||
|
||||
def _revert_plugin(plugin: "Plugin") -> None:
|
||||
if plugin.name not in _plugins:
|
||||
raise RuntimeError("Plugin not found!")
|
||||
del _plugins[plugin.name]
|
||||
|
||||
|
||||
def get_plugin(name: str) -> Optional["Plugin"]:
|
||||
"""获取已经导入的某个插件。
|
||||
|
||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||
|
||||
参数:
|
||||
name: 插件名,即 {ref}`nonebot.plugin.plugin.Plugin.name`。
|
||||
"""
|
||||
return _plugins.get(name)
|
||||
|
||||
|
||||
def get_plugin_by_module_name(module_name: str) -> Optional["Plugin"]:
|
||||
"""通过模块名获取已经导入的某个插件。
|
||||
|
||||
如果提供的模块名为某个插件的子模块,同样会返回该插件。
|
||||
|
||||
参数:
|
||||
module_name: 模块名,即 {ref}`nonebot.plugin.plugin.Plugin.module_name`。
|
||||
"""
|
||||
splits = module_name.split(".")
|
||||
loaded = {plugin.module_name: plugin for plugin in _plugins.values()}
|
||||
while splits:
|
||||
name = ".".join(splits)
|
||||
if name in loaded:
|
||||
return loaded[name]
|
||||
splits.pop()
|
||||
|
||||
|
||||
def get_loaded_plugins() -> Set["Plugin"]:
|
||||
"""获取当前已导入的所有插件。"""
|
||||
return set(_plugins.values())
|
||||
|
||||
|
||||
def get_available_plugin_names() -> Set[str]:
|
||||
"""获取当前所有可用的插件名(包含尚未加载的插件)。"""
|
||||
return {*chain.from_iterable(manager.available_plugins for manager in _managers)}
|
||||
|
||||
|
||||
from .on import on as on
|
||||
from .manager import PluginManager
|
||||
from .export import Export as Export
|
||||
@@ -58,14 +120,14 @@ from .on import on_endswith as on_endswith
|
||||
from .load import load_plugin as load_plugin
|
||||
from .on import CommandGroup as CommandGroup
|
||||
from .on import MatcherGroup as MatcherGroup
|
||||
from .on import on_fullmatch as on_fullmatch
|
||||
from .on import on_metaevent as on_metaevent
|
||||
from .plugin import get_plugin as get_plugin
|
||||
from .load import load_plugins as load_plugins
|
||||
from .on import on_startswith as on_startswith
|
||||
from .load import load_from_json as load_from_json
|
||||
from .load import load_from_toml as load_from_toml
|
||||
from .on import on_shell_command as on_shell_command
|
||||
from .plugin import PluginMetadata as PluginMetadata
|
||||
from .load import load_all_plugins as load_all_plugins
|
||||
from .load import load_builtin_plugin as load_builtin_plugin
|
||||
from .plugin import get_loaded_plugins as get_loaded_plugins
|
||||
from .load import load_builtin_plugins as load_builtin_plugins
|
||||
|
@@ -7,7 +7,9 @@ FrontMatter:
|
||||
description: nonebot.plugin.export 模块
|
||||
"""
|
||||
|
||||
from . import _current_plugin
|
||||
import warnings
|
||||
|
||||
from . import _current_plugin_chain
|
||||
|
||||
|
||||
class Export(dict):
|
||||
@@ -51,7 +53,12 @@ class Export(dict):
|
||||
|
||||
def export() -> Export:
|
||||
"""获取当前插件的导出内容对象"""
|
||||
plugin = _current_plugin.get()
|
||||
if not plugin:
|
||||
warnings.warn(
|
||||
"nonebot.export() is deprecated. "
|
||||
"See https://github.com/nonebot/nonebot2/issues/935.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
plugins = _current_plugin_chain.get()
|
||||
if not plugins:
|
||||
raise RuntimeError("Export outside of the plugin!")
|
||||
return plugin.export
|
||||
return plugins[-1].export
|
||||
|
@@ -10,10 +10,10 @@ from typing import Set, Iterable, Optional
|
||||
|
||||
import tomlkit
|
||||
|
||||
from . import _managers
|
||||
from .export import Export
|
||||
from .plugin import Plugin
|
||||
from .manager import PluginManager
|
||||
from .plugin import Plugin, get_plugin
|
||||
from . import _managers, get_plugin, _module_name_to_plugin_name
|
||||
|
||||
|
||||
def load_plugin(module_path: str) -> Optional[Plugin]:
|
||||
@@ -107,7 +107,7 @@ def load_from_toml(file_path: str, encoding: str = "utf-8") -> Set[Plugin]:
|
||||
nonebot_data = data.get("nonebot", {}).get("plugins")
|
||||
if nonebot_data:
|
||||
warnings.warn(
|
||||
"[nonebot.plugins] table are now deprecated. Use [tool.nonebot] instead.",
|
||||
"[nonebot.plugins] table is deprecated. Use [tool.nonebot] instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
else:
|
||||
@@ -128,7 +128,7 @@ def load_builtin_plugin(name: str) -> Optional[Plugin]:
|
||||
return load_plugin(f"nonebot.plugins.{name}")
|
||||
|
||||
|
||||
def load_builtin_plugins(*plugins) -> Set[Plugin]:
|
||||
def load_builtin_plugins(*plugins: str) -> Set[Plugin]:
|
||||
"""导入多个 NoneBot 内置插件。
|
||||
|
||||
参数:
|
||||
@@ -154,7 +154,7 @@ def require(name: str) -> Export:
|
||||
异常:
|
||||
RuntimeError: 插件无法加载
|
||||
"""
|
||||
plugin = get_plugin(name.rsplit(".", 1)[-1])
|
||||
plugin = get_plugin(_module_name_to_plugin_name(name))
|
||||
if not plugin:
|
||||
manager = _find_manager_by_name(name)
|
||||
if manager:
|
||||
|
@@ -19,23 +19,52 @@ from typing import Set, Dict, List, Union, Iterable, Optional, Sequence
|
||||
from nonebot.log import logger
|
||||
from nonebot.utils import escape_tag
|
||||
|
||||
from . import _managers, _current_plugin
|
||||
from .plugin import Plugin, _new_plugin, _confirm_plugin
|
||||
from .plugin import Plugin, PluginMetadata
|
||||
from . import (
|
||||
_managers,
|
||||
_new_plugin,
|
||||
_revert_plugin,
|
||||
_current_plugin_chain,
|
||||
_module_name_to_plugin_name,
|
||||
)
|
||||
|
||||
|
||||
class PluginManager:
|
||||
"""插件管理器。
|
||||
|
||||
参数:
|
||||
plugins: 独立插件模块名集合。
|
||||
search_path: 插件搜索路径(文件夹)。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
plugins: Optional[Iterable[str]] = None,
|
||||
search_path: Optional[Iterable[str]] = None,
|
||||
):
|
||||
|
||||
# simple plugin not in search path
|
||||
self.plugins: Set[str] = set(plugins or [])
|
||||
self.search_path: Set[str] = set(search_path or [])
|
||||
|
||||
# cache plugins
|
||||
self.searched_plugins: Dict[str, Path] = {}
|
||||
self.list_plugins()
|
||||
self._third_party_plugin_names: Dict[str, str] = {}
|
||||
self._searched_plugin_names: Dict[str, Path] = {}
|
||||
self.prepare_plugins()
|
||||
|
||||
@property
|
||||
def third_party_plugins(self) -> Set[str]:
|
||||
"""返回所有独立插件名称。"""
|
||||
return set(self._third_party_plugin_names.keys())
|
||||
|
||||
@property
|
||||
def searched_plugins(self) -> Set[str]:
|
||||
"""返回已搜索到的插件名称。"""
|
||||
return set(self._searched_plugin_names.keys())
|
||||
|
||||
@property
|
||||
def available_plugins(self) -> Set[str]:
|
||||
"""返回当前插件管理器中可用的插件名称。"""
|
||||
return self.third_party_plugins | self.searched_plugins
|
||||
|
||||
def _path_to_module_name(self, path: Path) -> str:
|
||||
rel_path = path.resolve().relative_to(Path(".").resolve())
|
||||
@@ -44,48 +73,51 @@ class PluginManager:
|
||||
else:
|
||||
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
||||
|
||||
def _previous_plugins(self) -> List[str]:
|
||||
def _previous_plugins(self) -> Set[str]:
|
||||
_pre_managers: List[PluginManager]
|
||||
if self in _managers:
|
||||
_pre_managers = _managers[: _managers.index(self)]
|
||||
else:
|
||||
_pre_managers = _managers[:]
|
||||
|
||||
return [
|
||||
*chain.from_iterable(
|
||||
[
|
||||
*map(lambda x: x.rsplit(".", 1)[-1], manager.plugins),
|
||||
*manager.searched_plugins.keys(),
|
||||
]
|
||||
for manager in _pre_managers
|
||||
)
|
||||
]
|
||||
return {
|
||||
*chain.from_iterable(manager.available_plugins for manager in _pre_managers)
|
||||
}
|
||||
|
||||
def prepare_plugins(self) -> Set[str]:
|
||||
"""搜索插件并缓存插件名称。"""
|
||||
|
||||
def list_plugins(self) -> Set[str]:
|
||||
# get all previous ready to load plugins
|
||||
previous_plugins = self._previous_plugins()
|
||||
searched_plugins: Dict[str, Path] = {}
|
||||
third_party_plugins: Set[str] = set()
|
||||
third_party_plugins: Dict[str, str] = {}
|
||||
|
||||
# check third party plugins
|
||||
for plugin in self.plugins:
|
||||
name = plugin.rsplit(".", 1)[-1]
|
||||
name = _module_name_to_plugin_name(plugin)
|
||||
if name in third_party_plugins or name in previous_plugins:
|
||||
raise RuntimeError(
|
||||
f"Plugin already exists: {name}! Check your plugin name"
|
||||
)
|
||||
third_party_plugins.add(plugin)
|
||||
third_party_plugins[name] = plugin
|
||||
|
||||
self._third_party_plugin_names = third_party_plugins
|
||||
|
||||
# check plugins in search path
|
||||
for module_info in pkgutil.iter_modules(self.search_path):
|
||||
# ignore if startswith "_"
|
||||
if module_info.name.startswith("_"):
|
||||
continue
|
||||
|
||||
if (
|
||||
module_info.name in searched_plugins.keys()
|
||||
module_info.name in searched_plugins
|
||||
or module_info.name in previous_plugins
|
||||
or module_info.name in third_party_plugins
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"Plugin already exists: {module_info.name}! Check your plugin name"
|
||||
)
|
||||
|
||||
module_spec = module_info.module_finder.find_spec(module_info.name, None)
|
||||
if not module_spec:
|
||||
continue
|
||||
@@ -94,17 +126,27 @@ class PluginManager:
|
||||
continue
|
||||
searched_plugins[module_info.name] = Path(module_path).resolve()
|
||||
|
||||
self.searched_plugins = searched_plugins
|
||||
self._searched_plugin_names = searched_plugins
|
||||
|
||||
return third_party_plugins | set(self.searched_plugins.keys())
|
||||
return self.available_plugins
|
||||
|
||||
def load_plugin(self, name: str) -> Optional[Plugin]:
|
||||
"""加载指定插件。
|
||||
|
||||
对于独立插件,可以使用完整插件模块名或者插件名称。
|
||||
|
||||
参数:
|
||||
name: 插件名称。
|
||||
"""
|
||||
|
||||
try:
|
||||
if name in self.plugins:
|
||||
module = importlib.import_module(name)
|
||||
elif name in self.searched_plugins:
|
||||
elif name in self._third_party_plugin_names:
|
||||
module = importlib.import_module(self._third_party_plugin_names[name])
|
||||
elif name in self._searched_plugin_names:
|
||||
module = importlib.import_module(
|
||||
self._path_to_module_name(self.searched_plugins[name])
|
||||
self._path_to_module_name(self._searched_plugin_names[name])
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"Plugin not found: {name}! Check your plugin name")
|
||||
@@ -125,8 +167,10 @@ class PluginManager:
|
||||
)
|
||||
|
||||
def load_all_plugins(self) -> Set[Plugin]:
|
||||
"""加载所有可用插件。"""
|
||||
|
||||
return set(
|
||||
filter(None, (self.load_plugin(name) for name in self.list_plugins()))
|
||||
filter(None, (self.load_plugin(name) for name in self.available_plugins))
|
||||
)
|
||||
|
||||
|
||||
@@ -147,9 +191,10 @@ class PluginFinder(MetaPathFinder):
|
||||
module_path = Path(module_origin).resolve()
|
||||
|
||||
for manager in reversed(_managers):
|
||||
# use path instead of name in case of submodule name conflict
|
||||
if (
|
||||
fullname in manager.plugins
|
||||
or module_path in manager.searched_plugins.values()
|
||||
or module_path in manager._searched_plugin_names.values()
|
||||
):
|
||||
module_spec.loader = PluginLoader(manager, fullname, module_origin)
|
||||
return module_spec
|
||||
@@ -173,29 +218,34 @@ class PluginLoader(SourceFileLoader):
|
||||
if self.loaded:
|
||||
return
|
||||
|
||||
# create plugin before executing
|
||||
plugin = _new_plugin(self.name, module, self.manager)
|
||||
parent_plugin = _current_plugin.get()
|
||||
if parent_plugin and _managers.index(parent_plugin.manager) < _managers.index(
|
||||
self.manager
|
||||
):
|
||||
plugin.parent_plugin = parent_plugin
|
||||
parent_plugin.sub_plugins.add(plugin)
|
||||
|
||||
_plugin_token = _current_plugin.set(plugin)
|
||||
|
||||
setattr(module, "__plugin__", plugin)
|
||||
|
||||
# try:
|
||||
# super().exec_module(module)
|
||||
# except Exception as e:
|
||||
# raise ImportError(
|
||||
# f"Error when executing module {module_name} from {module.__file__}."
|
||||
# ) from e
|
||||
super().exec_module(module)
|
||||
# detect parent plugin before entering current plugin context
|
||||
parent_plugins = _current_plugin_chain.get()
|
||||
for pre_plugin in reversed(parent_plugins):
|
||||
if _managers.index(pre_plugin.manager) < _managers.index(self.manager):
|
||||
plugin.parent_plugin = pre_plugin
|
||||
pre_plugin.sub_plugins.add(plugin)
|
||||
break
|
||||
|
||||
_confirm_plugin(plugin)
|
||||
# enter plugin context
|
||||
_plugin_token = _current_plugin_chain.set(parent_plugins + (plugin,))
|
||||
|
||||
try:
|
||||
super().exec_module(module)
|
||||
except Exception:
|
||||
_revert_plugin(plugin)
|
||||
raise
|
||||
finally:
|
||||
# leave plugin context
|
||||
_current_plugin_chain.reset(_plugin_token)
|
||||
|
||||
# get plugin metadata
|
||||
metadata: Optional[PluginMetadata] = getattr(module, "__plugin_meta__", None)
|
||||
plugin.metadata = metadata
|
||||
|
||||
_current_plugin.reset(_plugin_token)
|
||||
return
|
||||
|
||||
|
||||
|
@@ -5,9 +5,9 @@ FrontMatter:
|
||||
description: nonebot.plugin.on 模块
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
import inspect
|
||||
from types import ModuleType
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Set, Dict, List, Type, Tuple, Union, Optional
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
@@ -21,18 +21,19 @@ from nonebot.rule import (
|
||||
command,
|
||||
keyword,
|
||||
endswith,
|
||||
fullmatch,
|
||||
startswith,
|
||||
shell_command,
|
||||
)
|
||||
|
||||
from .manager import _current_plugin
|
||||
from .manager import _current_plugin_chain
|
||||
|
||||
|
||||
def _store_matcher(matcher: Type[Matcher]) -> None:
|
||||
plugin = _current_plugin.get()
|
||||
plugins = _current_plugin_chain.get()
|
||||
# only store the matcher defined in the plugin
|
||||
if plugin:
|
||||
plugin.matcher.add(matcher)
|
||||
if plugins:
|
||||
plugins[-1].matcher.add(matcher)
|
||||
|
||||
|
||||
def _get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
|
||||
@@ -40,8 +41,7 @@ def _get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
|
||||
if current_frame is None:
|
||||
return None
|
||||
frame = inspect.getouterframes(current_frame)[depth + 1].frame
|
||||
module_name = frame.f_globals["__name__"]
|
||||
return sys.modules.get(module_name)
|
||||
return inspect.getmodule(frame)
|
||||
|
||||
|
||||
def on(
|
||||
@@ -51,13 +51,13 @@ def on(
|
||||
*,
|
||||
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]:
|
||||
"""
|
||||
注册一个基础事件响应器,可自定义类型。
|
||||
"""注册一个基础事件响应器,可自定义类型。
|
||||
|
||||
参数:
|
||||
type: 事件响应器类型
|
||||
@@ -65,19 +65,22 @@ def on(
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
plugin_chain = _current_plugin_chain.get()
|
||||
matcher = Matcher.new(
|
||||
type,
|
||||
Rule() & rule,
|
||||
Permission() | permission,
|
||||
temp=temp,
|
||||
expire_time=expire_time,
|
||||
priority=priority,
|
||||
block=block,
|
||||
handlers=handlers,
|
||||
plugin=_current_plugin.get(),
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
@@ -90,31 +93,34 @@ def on_metaevent(
|
||||
*,
|
||||
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]:
|
||||
"""
|
||||
注册一个元事件响应器。
|
||||
"""注册一个元事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
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=_current_plugin.get(),
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
@@ -128,32 +134,35 @@ def on_message(
|
||||
*,
|
||||
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]:
|
||||
"""
|
||||
注册一个消息事件响应器。
|
||||
"""注册一个消息事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
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=_current_plugin.get(),
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
@@ -166,31 +175,34 @@ def on_notice(
|
||||
*,
|
||||
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]:
|
||||
"""
|
||||
注册一个通知事件响应器。
|
||||
"""注册一个通知事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
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=_current_plugin.get(),
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
@@ -203,31 +215,34 @@ def on_request(
|
||||
*,
|
||||
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]:
|
||||
"""
|
||||
注册一个请求事件响应器。
|
||||
"""注册一个请求事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
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=_current_plugin.get(),
|
||||
plugin=plugin_chain[-1] if plugin_chain else None,
|
||||
module=_get_matcher_module(_depth + 1),
|
||||
default_state=state,
|
||||
)
|
||||
@@ -237,13 +252,12 @@ def on_request(
|
||||
|
||||
def on_startswith(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = None,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
ignorecase: bool = False,
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
|
||||
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
|
||||
|
||||
参数:
|
||||
msg: 指定消息开头内容
|
||||
@@ -252,6 +266,7 @@ def on_startswith(
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -261,13 +276,12 @@ def on_startswith(
|
||||
|
||||
def on_endswith(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = None,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
ignorecase: bool = False,
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
|
||||
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
|
||||
|
||||
参数:
|
||||
msg: 指定消息结尾内容
|
||||
@@ -276,6 +290,7 @@ def on_endswith(
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -283,14 +298,37 @@ def on_endswith(
|
||||
return on_message(endswith(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_fullmatch(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
ignorecase: bool = False,
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
|
||||
|
||||
参数:
|
||||
msg: 指定消息全匹配内容
|
||||
rule: 事件响应规则
|
||||
ignorecase: 是否忽略大小写
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
return on_message(fullmatch(msg, ignorecase) & rule, **kwargs, _depth=_depth + 1)
|
||||
|
||||
|
||||
def on_keyword(
|
||||
keywords: Set[str],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
|
||||
"""注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
|
||||
|
||||
参数:
|
||||
keywords: 关键词列表
|
||||
@@ -298,6 +336,7 @@ def on_keyword(
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -312,8 +351,7 @@ def on_command(
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
||||
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
||||
|
||||
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
|
||||
|
||||
@@ -324,6 +362,7 @@ def on_command(
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -344,8 +383,7 @@ def on_shell_command(
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
|
||||
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
|
||||
|
||||
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
|
||||
|
||||
@@ -359,6 +397,7 @@ def on_shell_command(
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -379,8 +418,7 @@ def on_regex(
|
||||
_depth: int = 0,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
|
||||
"""注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
|
||||
|
||||
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
|
||||
|
||||
@@ -391,6 +429,7 @@ def on_regex(
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -399,32 +438,42 @@ def on_regex(
|
||||
|
||||
|
||||
class CommandGroup:
|
||||
"""命令组,用于声明一组有相同名称前缀的命令。"""
|
||||
"""命令组,用于声明一组有相同名称前缀的命令。
|
||||
|
||||
参数:
|
||||
cmd: 指定命令内容
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
|
||||
def __init__(self, cmd: Union[str, Tuple[str, ...]], **kwargs):
|
||||
"""
|
||||
参数:
|
||||
cmd: 命令前缀
|
||||
**kwargs: `on_command` 的参数默认值,参考 `on_command <#on-command-cmd-rule-none-aliases-none-kwargs>`_
|
||||
"""
|
||||
self.basecmd: Tuple[str, ...] = (cmd,) if isinstance(cmd, str) else cmd
|
||||
"""
|
||||
命令前缀
|
||||
"""
|
||||
"""命令前缀"""
|
||||
if "aliases" in kwargs:
|
||||
del kwargs["aliases"]
|
||||
self.base_kwargs: Dict[str, Any] = kwargs
|
||||
"""
|
||||
其他传递给 `on_command` 的参数默认值
|
||||
"""
|
||||
"""其他传递给 `on_command` 的参数默认值"""
|
||||
|
||||
def command(self, cmd: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个新的命令。
|
||||
"""注册一个新的命令。新参数将会覆盖命令组默认值
|
||||
|
||||
参数:
|
||||
cmd: 命令前缀
|
||||
**kwargs: `on_command` 的参数,将会覆盖命令组默认值
|
||||
cmd: 指定命令内容
|
||||
aliases: 命令别名
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
|
||||
cmd = self.basecmd + sub_cmd
|
||||
@@ -436,12 +485,20 @@ class CommandGroup:
|
||||
def shell_command(
|
||||
self, cmd: Union[str, Tuple[str, ...]], **kwargs
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个新的命令。
|
||||
"""注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值
|
||||
|
||||
参数:
|
||||
cmd: 命令前缀
|
||||
**kwargs: `on_shell_command` 的参数,将会覆盖命令组默认值
|
||||
cmd: 指定命令内容
|
||||
rule: 事件响应规则
|
||||
aliases: 命令别名
|
||||
parser: `nonebot.rule.ArgumentParser` 对象
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
sub_cmd = (cmd,) if isinstance(cmd, str) else cmd
|
||||
cmd = self.basecmd + sub_cmd
|
||||
@@ -455,21 +512,14 @@ class MatcherGroup:
|
||||
"""事件响应器组合,统一管理。为 `Matcher` 创建提供默认属性。"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
创建一个事件响应器组合,参数为默认值,与 `on` 一致
|
||||
"""
|
||||
"""创建一个事件响应器组合,参数为默认值,与 `on` 一致"""
|
||||
self.matchers: List[Type[Matcher]] = []
|
||||
"""
|
||||
组内事件响应器列表
|
||||
"""
|
||||
"""组内事件响应器列表"""
|
||||
self.base_kwargs: Dict[str, Any] = kwargs
|
||||
"""
|
||||
其他传递给 `on` 的参数默认值
|
||||
"""
|
||||
"""其他传递给 `on` 的参数默认值"""
|
||||
|
||||
def on(self, **kwargs) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个基础事件响应器,可自定义类型。
|
||||
"""注册一个基础事件响应器,可自定义类型。
|
||||
|
||||
参数:
|
||||
type: 事件响应器类型
|
||||
@@ -477,6 +527,7 @@ class MatcherGroup:
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -488,13 +539,13 @@ class MatcherGroup:
|
||||
return matcher
|
||||
|
||||
def on_metaevent(self, **kwargs) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个元事件响应器。
|
||||
"""注册一个元事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -508,14 +559,14 @@ class MatcherGroup:
|
||||
return matcher
|
||||
|
||||
def on_message(self, **kwargs) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器。
|
||||
"""注册一个消息事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -528,13 +579,13 @@ class MatcherGroup:
|
||||
return matcher
|
||||
|
||||
def on_notice(self, **kwargs) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个通知事件响应器。
|
||||
"""注册一个通知事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -547,13 +598,13 @@ class MatcherGroup:
|
||||
return matcher
|
||||
|
||||
def on_request(self, **kwargs) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个请求事件响应器。
|
||||
"""注册一个请求事件响应器。
|
||||
|
||||
参数:
|
||||
rule: 事件响应规则
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -568,8 +619,7 @@ class MatcherGroup:
|
||||
def on_startswith(
|
||||
self, msg: Union[str, Tuple[str, ...]], **kwargs
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
|
||||
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。
|
||||
|
||||
参数:
|
||||
msg: 指定消息开头内容
|
||||
@@ -578,6 +628,7 @@ class MatcherGroup:
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -590,8 +641,7 @@ class MatcherGroup:
|
||||
return matcher
|
||||
|
||||
def on_endswith(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
|
||||
"""注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。
|
||||
|
||||
参数:
|
||||
msg: 指定消息结尾内容
|
||||
@@ -600,6 +650,7 @@ class MatcherGroup:
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -611,9 +662,30 @@ class MatcherGroup:
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
def on_keyword(self, keywords: Set[str], **kwargs) -> Type[Matcher]:
|
||||
def on_fullmatch(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
|
||||
"""注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
|
||||
|
||||
参数:
|
||||
msg: 指定消息全匹配内容
|
||||
rule: 事件响应规则
|
||||
ignorecase: 是否忽略大小写
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
|
||||
final_kwargs = self.base_kwargs.copy()
|
||||
final_kwargs.update(kwargs)
|
||||
final_kwargs.pop("type", None)
|
||||
matcher = on_fullmatch(msg, **final_kwargs, _depth=1)
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
def on_keyword(self, keywords: Set[str], **kwargs) -> Type[Matcher]:
|
||||
"""注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
|
||||
|
||||
参数:
|
||||
keywords: 关键词列表
|
||||
@@ -621,6 +693,7 @@ class MatcherGroup:
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -638,8 +711,7 @@ class MatcherGroup:
|
||||
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
||||
"""注册一个消息事件响应器,并且当消息以指定命令开头时响应。
|
||||
|
||||
命令匹配规则参考: `命令形式匹配 <rule.md#command-command>`_
|
||||
|
||||
@@ -650,6 +722,7 @@ class MatcherGroup:
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -668,8 +741,7 @@ class MatcherGroup:
|
||||
parser: Optional[ArgumentParser] = None,
|
||||
**kwargs,
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
|
||||
"""注册一个支持 `shell_like` 解析参数的命令消息事件响应器。
|
||||
|
||||
与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。
|
||||
|
||||
@@ -683,6 +755,7 @@ class MatcherGroup:
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
@@ -699,8 +772,7 @@ class MatcherGroup:
|
||||
def on_regex(
|
||||
self, pattern: str, flags: Union[int, re.RegexFlag] = 0, **kwargs
|
||||
) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
|
||||
"""注册一个消息事件响应器,并且当消息匹配正则表达式时响应。
|
||||
|
||||
命令匹配规则参考: `正则匹配 <rule.md#regex-regex-flags-0>`_
|
||||
|
||||
@@ -711,6 +783,7 @@ class MatcherGroup:
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
expire_time: 事件响应器最终有效时间点,过时即被删除
|
||||
priority: 事件响应器优先级
|
||||
block: 是否阻止事件向更低优先级传递
|
||||
state: 默认 state
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Set, List, Type, Tuple, Union, Optional
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
@@ -14,6 +15,7 @@ def on(
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -23,6 +25,7 @@ def on_metaevent(
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -33,6 +36,7 @@ def on_message(
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -42,6 +46,7 @@ def on_notice(
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -51,30 +56,46 @@ def on_request(
|
||||
*,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_startswith(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ...,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
ignorecase: bool = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_endswith(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ...,
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
ignorecase: bool = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_fullmatch(
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
ignorecase: bool = ...,
|
||||
*,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -86,6 +107,7 @@ def on_keyword(
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -98,6 +120,7 @@ def on_command(
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -111,6 +134,7 @@ def on_shell_command(
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -123,6 +147,7 @@ def on_regex(
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -137,6 +162,7 @@ class CommandGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -150,6 +176,7 @@ class CommandGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -164,6 +191,7 @@ class CommandGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -178,6 +206,7 @@ class MatcherGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -190,6 +219,7 @@ class MatcherGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -200,6 +230,7 @@ class MatcherGroup:
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -211,6 +242,7 @@ class MatcherGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -221,6 +253,7 @@ class MatcherGroup:
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -231,6 +264,7 @@ class MatcherGroup:
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -244,6 +278,7 @@ class MatcherGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -257,6 +292,21 @@ class MatcherGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_fullmatch(
|
||||
self,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
*,
|
||||
ignorecase: bool = ...,
|
||||
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]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -269,6 +319,7 @@ class MatcherGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -282,6 +333,7 @@ class MatcherGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -296,6 +348,7 @@ class MatcherGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
@@ -309,6 +362,7 @@ class MatcherGroup:
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = ...,
|
||||
handlers: Optional[List[Union[T_Handler, Dependent]]] = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
|
@@ -6,25 +6,40 @@ FrontMatter:
|
||||
"""
|
||||
from types import ModuleType
|
||||
from dataclasses import field, dataclass
|
||||
from typing import TYPE_CHECKING, Set, Dict, Type, Optional
|
||||
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
from .export import Export
|
||||
from . import _plugins as plugins # FIXME: backport for nonebug
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import PluginManager
|
||||
|
||||
plugins: Dict[str, "Plugin"] = {}
|
||||
"""已加载的插件"""
|
||||
|
||||
@dataclass(eq=False)
|
||||
class PluginMetadata:
|
||||
"""插件元信息,由插件编写者提供"""
|
||||
|
||||
name: str
|
||||
"""插件可阅读名称"""
|
||||
description: str
|
||||
"""插件功能介绍"""
|
||||
usage: str
|
||||
"""插件使用方法"""
|
||||
config: Optional[Type[BaseModel]] = None
|
||||
"""插件配置项"""
|
||||
extra: Dict[Any, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class Plugin(object):
|
||||
class Plugin:
|
||||
"""存储插件信息"""
|
||||
|
||||
name: str
|
||||
"""插件名称,使用 文件/文件夹 名称作为插件名"""
|
||||
"""插件索引标识,NoneBot 使用 文件/文件夹 名称作为标识符"""
|
||||
module: ModuleType
|
||||
"""插件模块对象"""
|
||||
module_name: str
|
||||
@@ -32,40 +47,11 @@ class Plugin(object):
|
||||
manager: "PluginManager"
|
||||
"""导入该插件的插件管理器"""
|
||||
export: Export = field(default_factory=Export)
|
||||
"""插件内定义的导出内容"""
|
||||
"""**Deprecated:** 插件内定义的导出内容"""
|
||||
matcher: Set[Type[Matcher]] = field(default_factory=set)
|
||||
"""插件内定义的 `Matcher`"""
|
||||
parent_plugin: Optional["Plugin"] = None
|
||||
"""父插件"""
|
||||
sub_plugins: Set["Plugin"] = field(default_factory=set)
|
||||
"""子插件集合"""
|
||||
|
||||
|
||||
def get_plugin(name: str) -> Optional[Plugin]:
|
||||
"""获取已经导入的某个插件。
|
||||
|
||||
如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。
|
||||
|
||||
参数:
|
||||
name: 插件名,即 {ref}`nonebot.plugin.plugin.Plugin.name`。
|
||||
"""
|
||||
return plugins.get(name)
|
||||
|
||||
|
||||
def get_loaded_plugins() -> Set[Plugin]:
|
||||
"""获取当前已导入的所有插件。"""
|
||||
return set(plugins.values())
|
||||
|
||||
|
||||
def _new_plugin(fullname: str, module: ModuleType, manager: "PluginManager") -> Plugin:
|
||||
name = fullname.rsplit(".", 1)[-1] if "." in fullname else fullname
|
||||
if name in plugins:
|
||||
raise RuntimeError("Plugin already exists! Check your plugin name.")
|
||||
plugin = Plugin(name, module, fullname, manager)
|
||||
return plugin
|
||||
|
||||
|
||||
def _confirm_plugin(plugin: Plugin) -> None:
|
||||
if plugin.name in plugins:
|
||||
raise RuntimeError("Plugin already exists! Check your plugin name.")
|
||||
plugins[plugin.name] = plugin
|
||||
metadata: Optional[PluginMetadata] = None
|
||||
|
@@ -14,7 +14,7 @@ from itertools import product
|
||||
from argparse import Namespace
|
||||
from typing_extensions import TypedDict
|
||||
from argparse import ArgumentParser as ArgParser
|
||||
from typing import Any, List, Tuple, Union, Optional, Sequence
|
||||
from typing import Any, List, Tuple, Union, Optional, Sequence, NamedTuple
|
||||
|
||||
from pygtrie import CharTrie
|
||||
|
||||
@@ -41,6 +41,7 @@ from nonebot.consts import (
|
||||
CMD_ARG_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
CMD_START_KEY,
|
||||
REGEX_MATCHED,
|
||||
)
|
||||
|
||||
@@ -50,15 +51,20 @@ CMD_RESULT = TypedDict(
|
||||
"command": Optional[Tuple[str, ...]],
|
||||
"raw_command": Optional[str],
|
||||
"command_arg": Optional[Message[MessageSegment]],
|
||||
"command_start": Optional[str],
|
||||
},
|
||||
)
|
||||
|
||||
TRIE_VALUE = NamedTuple(
|
||||
"TRIE_VALUE", [("command_start", str), ("command", Tuple[str, ...])]
|
||||
)
|
||||
|
||||
|
||||
class TrieRule:
|
||||
prefix: CharTrie = CharTrie()
|
||||
|
||||
@classmethod
|
||||
def add_prefix(cls, prefix: str, value: Any):
|
||||
def add_prefix(cls, prefix: str, value: TRIE_VALUE) -> None:
|
||||
if prefix in cls.prefix:
|
||||
logger.warning(f'Duplicated prefix rule "{prefix}"')
|
||||
return
|
||||
@@ -66,7 +72,9 @@ 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)
|
||||
prefix = CMD_RESULT(
|
||||
command=None, raw_command=None, command_arg=None, command_start=None
|
||||
)
|
||||
state[PREFIX_KEY] = prefix
|
||||
if event.get_type() != "message":
|
||||
return prefix
|
||||
@@ -76,9 +84,11 @@ class TrieRule:
|
||||
if message_seg.is_text():
|
||||
segment_text = str(message_seg).lstrip()
|
||||
pf = cls.prefix.longest_prefix(segment_text)
|
||||
prefix[RAW_CMD_KEY] = pf.key
|
||||
prefix[CMD_KEY] = pf.value
|
||||
if pf.key:
|
||||
if pf:
|
||||
value: TRIE_VALUE = pf.value
|
||||
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())
|
||||
@@ -171,6 +181,42 @@ def endswith(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule
|
||||
return Rule(EndswithRule(msg, ignorecase))
|
||||
|
||||
|
||||
class FullmatchRule:
|
||||
"""检查消息纯文本是否与指定字符串全匹配。
|
||||
|
||||
参数:
|
||||
msg: 指定消息全匹配字符串元组
|
||||
ignorecase: 是否忽略大小写
|
||||
"""
|
||||
|
||||
__slots__ = ("msg", "ignorecase")
|
||||
|
||||
def __init__(self, msg: Tuple[str, ...], ignorecase: bool = False):
|
||||
self.msg = frozenset(map(str.casefold, msg) if ignorecase else msg)
|
||||
self.ignorecase = ignorecase
|
||||
|
||||
async def __call__(
|
||||
self, type_: str = EventType(), text: str = EventPlainText()
|
||||
) -> bool:
|
||||
return (
|
||||
type_ == "message"
|
||||
and (text.casefold() if self.ignorecase else text) in self.msg
|
||||
)
|
||||
|
||||
|
||||
def fullmatch(msg: Union[str, Tuple[str, ...]], ignorecase: bool = False) -> Rule:
|
||||
"""完全匹配消息。
|
||||
|
||||
参数:
|
||||
msg: 指定消息全匹配字符串元组
|
||||
ignorecase: 是否忽略大小写
|
||||
"""
|
||||
if isinstance(msg, str):
|
||||
msg = (msg,)
|
||||
|
||||
return Rule(FullmatchRule(msg, ignorecase))
|
||||
|
||||
|
||||
class KeywordsRule:
|
||||
"""检查消息纯文本是否包含指定关键字。
|
||||
|
||||
@@ -256,10 +302,12 @@ def command(*cmds: Union[str, Tuple[str, ...]]) -> Rule:
|
||||
|
||||
if len(command) == 1:
|
||||
for start in command_start:
|
||||
TrieRule.add_prefix(f"{start}{command[0]}", command)
|
||||
TrieRule.add_prefix(f"{start}{command[0]}", TRIE_VALUE(start, command))
|
||||
else:
|
||||
for start, sep in product(command_start, command_sep):
|
||||
TrieRule.add_prefix(f"{start}{sep.join(command)}", command)
|
||||
TrieRule.add_prefix(
|
||||
f"{start}{sep.join(command)}", TRIE_VALUE(start, command)
|
||||
)
|
||||
|
||||
return Rule(CommandRule(commands))
|
||||
|
||||
@@ -380,10 +428,12 @@ def shell_command(
|
||||
|
||||
if len(command) == 1:
|
||||
for start in command_start:
|
||||
TrieRule.add_prefix(f"{start}{command[0]}", command)
|
||||
TrieRule.add_prefix(f"{start}{command[0]}", TRIE_VALUE(start, command))
|
||||
else:
|
||||
for start, sep in product(command_start, command_sep):
|
||||
TrieRule.add_prefix(f"{start}{sep.join(command)}", command)
|
||||
TrieRule.add_prefix(
|
||||
f"{start}{sep.join(command)}", TRIE_VALUE(start, command)
|
||||
)
|
||||
|
||||
return Rule(ShellCommandRule(commands, parser))
|
||||
|
||||
|
@@ -45,9 +45,23 @@ T_State = Dict[Any, Any]
|
||||
"""事件处理状态 State 类型"""
|
||||
|
||||
T_BotConnectionHook = Callable[..., Awaitable[Any]]
|
||||
"""Bot 连接建立时钩子函数"""
|
||||
"""Bot 连接建立时钩子函数
|
||||
|
||||
依赖参数:
|
||||
|
||||
- DependParam: 子依赖参数
|
||||
- BotParam: Bot 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_BotDisconnectionHook = Callable[..., Awaitable[Any]]
|
||||
"""Bot 连接断开时钩子函数"""
|
||||
"""Bot 连接断开时钩子函数
|
||||
|
||||
依赖参数:
|
||||
|
||||
- DependParam: 子依赖参数
|
||||
- BotParam: Bot 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
|
||||
"""`bot.call_api` 钩子函数"""
|
||||
T_CalledAPIHook = Callable[
|
||||
@@ -90,7 +104,7 @@ T_RunPreProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_RunPostProcessor = Callable[..., Union[Any, Awaitable[Any]]]
|
||||
"""事件响应器运行前预处理函数 RunPostProcessor 类型
|
||||
"""事件响应器运行后后处理函数 RunPostProcessor 类型
|
||||
|
||||
依赖参数:
|
||||
|
||||
|
@@ -21,7 +21,6 @@ from typing import (
|
||||
TypeVar,
|
||||
Callable,
|
||||
Optional,
|
||||
Awaitable,
|
||||
Coroutine,
|
||||
AsyncGenerator,
|
||||
ContextManager,
|
||||
@@ -63,12 +62,10 @@ def generic_check_issubclass(
|
||||
except TypeError:
|
||||
origin = get_origin(cls)
|
||||
if is_union(origin):
|
||||
for type_ in get_args(cls):
|
||||
if not is_none_type(type_) and not generic_check_issubclass(
|
||||
type_, class_or_tuple
|
||||
):
|
||||
return False
|
||||
return True
|
||||
return all(
|
||||
is_none_type(type_) or generic_check_issubclass(type_, class_or_tuple)
|
||||
for type_ in get_args(cls)
|
||||
)
|
||||
elif origin:
|
||||
return issubclass(origin, class_or_tuple)
|
||||
return False
|
||||
@@ -132,6 +129,17 @@ async def run_sync_ctx_manager(
|
||||
await run_sync(cm.__exit__)(None, None, None)
|
||||
|
||||
|
||||
async def run_coro_with_catch(
|
||||
coro: Coroutine[Any, Any, T],
|
||||
exc: Tuple[Type[Exception], ...],
|
||||
return_on_err: R = None,
|
||||
) -> Union[T, R]:
|
||||
try:
|
||||
return await coro
|
||||
except exc:
|
||||
return return_on_err
|
||||
|
||||
|
||||
def get_name(obj: Any) -> str:
|
||||
"""获取对象的名称"""
|
||||
if inspect.isfunction(obj) or inspect.isclass(obj):
|
||||
@@ -145,7 +153,7 @@ class DataclassEncoder(json.JSONEncoder):
|
||||
@overrides(json.JSONEncoder)
|
||||
def default(self, o):
|
||||
if dataclasses.is_dataclass(o):
|
||||
return dataclasses.asdict(o)
|
||||
return {f.name: getattr(o, f.name) for f in dataclasses.fields(o)}
|
||||
return super().default(o)
|
||||
|
||||
|
||||
|
1340
poetry.lock
generated
1340
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "nonebot2"
|
||||
version = "2.0.0-beta.2"
|
||||
version = "2.0.0-beta.5"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
@@ -26,23 +26,24 @@ python = "^3.7.3"
|
||||
yarl = "^1.7.2"
|
||||
loguru = "^0.6.0"
|
||||
pygtrie = "^2.4.1"
|
||||
tomlkit = "^0.9.0"
|
||||
fastapi = "^0.73.0"
|
||||
fastapi = "^0.79.0"
|
||||
tomlkit = ">=0.10.0,<1.0.0"
|
||||
typing-extensions = ">=3.10.0,<5.0.0"
|
||||
Quart = { version = "^0.16.0", optional = true }
|
||||
Quart = { version = "^0.17.0", optional = true }
|
||||
websockets = { version="^10.0", optional = true }
|
||||
pydantic = { version = "~1.9.0", extras = ["dotenv"] }
|
||||
uvicorn = { version = "^0.17.0", extras = ["standard"] }
|
||||
uvicorn = { version = "^0.18.0", extras = ["standard"] }
|
||||
aiohttp = { version = "^3.7.4", extras = ["speedups"], optional = true }
|
||||
httpx = { version = ">=0.20.0, <1.0.0", extras = ["http2"], optional = true }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
isort = "^5.10.1"
|
||||
black = "^22.1.0"
|
||||
nonemoji = "^0.1.2"
|
||||
pytest-cov = "^3.0.0"
|
||||
pre-commit = "^2.16.0"
|
||||
pytest-xdist = "^2.5.0"
|
||||
pytest-asyncio = "^0.18.1"
|
||||
pytest-asyncio = "^0.19.0"
|
||||
nonebug = { git = "https://github.com/nonebot/nonebug.git" }
|
||||
nb-autodoc = { git = "https://github.com/nonebot/nb-autodoc.git" }
|
||||
|
||||
@@ -53,14 +54,13 @@ aiohttp = ["aiohttp"]
|
||||
websockets = ["websockets"]
|
||||
all = ["quart", "aiohttp", "httpx", "websockets"]
|
||||
|
||||
# [[tool.poetry.source]]
|
||||
# name = "aliyun"
|
||||
# url = "https://mirrors.aliyun.com/pypi/simple/"
|
||||
# default = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
addopts = "--cov=nonebot --cov-report=term-missing"
|
||||
filterwarnings = [
|
||||
"error",
|
||||
"ignore::DeprecationWarning",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
|
@@ -1,3 +1,5 @@
|
||||
LOG_LEVEL=TRACE
|
||||
NICKNAME=["test"]
|
||||
SUPERUSERS=["test", "fake:faketest"]
|
||||
CONFIG_FROM_ENV=
|
||||
CONFIG_OVERRIDE=old
|
||||
|
6
tests/bad_plugins/bad_plugin.py
Normal file
6
tests/bad_plugins/bad_plugin.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import nonebot
|
||||
|
||||
plugin = nonebot.get_plugin("bad_plugin")
|
||||
assert plugin
|
||||
|
||||
x = 1 / 0
|
1
tests/plugins/_hidden.py
Normal file
1
tests/plugins/_hidden.py
Normal file
@@ -0,0 +1 @@
|
||||
assert False
|
@@ -3,4 +3,4 @@ from nonebot import export
|
||||
|
||||
@export()
|
||||
def test():
|
||||
...
|
||||
return "export"
|
||||
|
7
tests/plugins/matcher/matcher_expire.py
Normal file
7
tests/plugins/matcher/matcher_expire.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
|
||||
test_temp_matcher = Matcher.new("test", temp=True)
|
||||
test_datetime_matcher = Matcher.new("test", expire_time=datetime.now())
|
||||
test_timedelta_matcher = Matcher.new("test", expire_time=timedelta(seconds=-1))
|
16
tests/plugins/metadata.py
Normal file
16
tests/plugins/metadata.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
custom: str = ""
|
||||
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="测试插件",
|
||||
description="测试插件元信息",
|
||||
usage="无法使用",
|
||||
config=Config,
|
||||
extra={"author": "NoneBot"},
|
||||
)
|
13
tests/plugins/nested/__init__.py
Normal file
13
tests/plugins/nested/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from pathlib import Path
|
||||
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
|
||||
manager = PluginManager(
|
||||
search_path=[str((Path(__file__).parent / "plugins").resolve())]
|
||||
)
|
||||
_managers.append(manager)
|
||||
|
||||
# test load nested plugin with require
|
||||
manager.load_plugin("nested_subplugin")
|
||||
manager.load_plugin("nested_subplugin2")
|
1
tests/plugins/nested/plugins/nested_subplugin.py
Normal file
1
tests/plugins/nested/plugins/nested_subplugin.py
Normal file
@@ -0,0 +1 @@
|
||||
from .nested_subplugin2 import a
|
1
tests/plugins/nested/plugins/nested_subplugin2.py
Normal file
1
tests/plugins/nested/plugins/nested_subplugin2.py
Normal file
@@ -0,0 +1 @@
|
||||
a = "required by another subplugin"
|
@@ -1,5 +1,35 @@
|
||||
from typing import Union
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
|
||||
async def get_bot(b: Bot):
|
||||
async def get_bot(b: Bot) -> Bot:
|
||||
return b
|
||||
|
||||
|
||||
async def legacy_bot(bot):
|
||||
return bot
|
||||
|
||||
|
||||
async def not_legacy_bot(bot: int):
|
||||
...
|
||||
|
||||
|
||||
class FooBot(Bot):
|
||||
...
|
||||
|
||||
|
||||
async def sub_bot(b: FooBot) -> FooBot:
|
||||
return b
|
||||
|
||||
|
||||
class BarBot(Bot):
|
||||
...
|
||||
|
||||
|
||||
async def union_bot(b: Union[FooBot, BarBot]) -> Union[FooBot, BarBot]:
|
||||
return b
|
||||
|
||||
|
||||
async def not_bot(b: Union[int, Bot]):
|
||||
...
|
||||
|
@@ -1,3 +1,5 @@
|
||||
from typing import Union
|
||||
|
||||
from nonebot.adapters import Event, Message
|
||||
from nonebot.params import EventToMe, EventType, EventMessage, EventPlainText
|
||||
|
||||
@@ -6,6 +8,34 @@ async def event(e: Event) -> Event:
|
||||
return e
|
||||
|
||||
|
||||
async def legacy_event(event):
|
||||
return event
|
||||
|
||||
|
||||
async def not_legacy_event(event: int):
|
||||
...
|
||||
|
||||
|
||||
class FooEvent(Event):
|
||||
...
|
||||
|
||||
|
||||
async def sub_event(e: FooEvent) -> FooEvent:
|
||||
return e
|
||||
|
||||
|
||||
class BarEvent(Event):
|
||||
...
|
||||
|
||||
|
||||
async def union_event(e: Union[FooEvent, BarEvent]) -> Union[FooEvent, BarEvent]:
|
||||
return e
|
||||
|
||||
|
||||
async def not_event(e: Union[int, Event]):
|
||||
...
|
||||
|
||||
|
||||
async def event_type(t: str = EventType()) -> str:
|
||||
return t
|
||||
|
||||
|
@@ -8,6 +8,7 @@ from nonebot.params import (
|
||||
CommandArg,
|
||||
RawCommand,
|
||||
RegexGroup,
|
||||
CommandStart,
|
||||
RegexMatched,
|
||||
ShellCommandArgs,
|
||||
ShellCommandArgv,
|
||||
@@ -18,6 +19,14 @@ async def state(x: T_State) -> T_State:
|
||||
return x
|
||||
|
||||
|
||||
async def legacy_state(state):
|
||||
return state
|
||||
|
||||
|
||||
async def not_legacy_state(state: int):
|
||||
...
|
||||
|
||||
|
||||
async def command(cmd: Tuple[str, ...] = Command()) -> Tuple[str, ...]:
|
||||
return cmd
|
||||
|
||||
@@ -30,6 +39,10 @@ async def command_arg(cmd_arg: Message = CommandArg()) -> Message:
|
||||
return cmd_arg
|
||||
|
||||
|
||||
async def command_start(start: str = CommandStart()) -> str:
|
||||
return start
|
||||
|
||||
|
||||
async def shell_command_args(
|
||||
shell_command_args: dict = ShellCommandArgs(),
|
||||
) -> dict:
|
||||
|
@@ -1,8 +1,7 @@
|
||||
from nonebot import require
|
||||
from plugins.export import test
|
||||
|
||||
from .export import test as test_related
|
||||
|
||||
test_require = require("export").test
|
||||
|
||||
assert test is test_related and test is test_require, "Export Require Error"
|
||||
from plugins.export import test
|
||||
|
||||
assert test is test_require and test() == "export", "Export Require Error"
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError, parse_obj_as
|
||||
|
||||
from utils import make_fake_message
|
||||
@@ -29,14 +30,15 @@ def test_segment_validate():
|
||||
MessageSegment = Message.get_segment_class()
|
||||
|
||||
assert parse_obj_as(
|
||||
MessageSegment, {"type": "text", "data": {"text": "text"}}
|
||||
MessageSegment,
|
||||
{"type": "text", "data": {"text": "text"}, "extra": "should be ignored"},
|
||||
) == MessageSegment.text("text")
|
||||
|
||||
try:
|
||||
with pytest.raises(ValidationError):
|
||||
parse_obj_as(MessageSegment, "some str")
|
||||
assert False
|
||||
except ValidationError:
|
||||
assert True
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
parse_obj_as(MessageSegment, {"data": {}})
|
||||
|
||||
|
||||
def test_segment():
|
||||
@@ -129,11 +131,8 @@ def test_message_validate():
|
||||
|
||||
assert parse_obj_as(Message, Message([])) == Message([])
|
||||
|
||||
try:
|
||||
with pytest.raises(ValidationError):
|
||||
parse_obj_as(Message, Message_([]))
|
||||
assert False
|
||||
except ValidationError:
|
||||
assert True
|
||||
|
||||
assert parse_obj_as(Message, "text") == Message([MessageSegment.text("text")])
|
||||
|
||||
@@ -146,8 +145,5 @@ def test_message_validate():
|
||||
[MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
|
||||
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])
|
||||
|
||||
try:
|
||||
with pytest.raises(ValidationError):
|
||||
parse_obj_as(Message, object())
|
||||
assert False
|
||||
except ValidationError:
|
||||
assert True
|
||||
|
@@ -32,6 +32,26 @@ def test_template_message():
|
||||
assert str(formatted) == "custom-custom!text[fake:image]"
|
||||
|
||||
|
||||
def test_rich_template_message():
|
||||
Message = make_fake_message()
|
||||
MS = Message.get_segment_class()
|
||||
|
||||
pic1, pic2, pic3 = (
|
||||
MS.image("file:///pic1.jpg"),
|
||||
MS.image("file:///pic2.jpg"),
|
||||
MS.image("file:///pic3.jpg"),
|
||||
)
|
||||
|
||||
template = Message.template("{}{}" + pic2 + "{}")
|
||||
|
||||
result = template.format(pic1, "[fake:image]", pic3)
|
||||
|
||||
assert result["image"] == Message([pic1, pic2, pic3])
|
||||
assert str(result) == (
|
||||
"[fake:image]" + escape_text("[fake:image]") + "[fake:image]" + "[fake:image]"
|
||||
)
|
||||
|
||||
|
||||
def test_message_injection():
|
||||
Message = make_fake_message()
|
||||
|
||||
|
@@ -15,6 +15,7 @@ from nonebug import App
|
||||
)
|
||||
async def test_reverse_driver(app: App):
|
||||
import nonebot
|
||||
from nonebot.exception import WebSocketClosed
|
||||
from nonebot.drivers import (
|
||||
URL,
|
||||
Request,
|
||||
@@ -36,7 +37,21 @@ async def test_reverse_driver(app: App):
|
||||
data = await ws.receive()
|
||||
assert data == "ping"
|
||||
await ws.send("pong")
|
||||
await ws.close()
|
||||
|
||||
data = await ws.receive()
|
||||
assert data == b"ping"
|
||||
await ws.send(b"pong")
|
||||
|
||||
data = await ws.receive_text()
|
||||
assert data == "ping"
|
||||
await ws.send("pong")
|
||||
|
||||
data = await ws.receive_bytes()
|
||||
assert data == b"ping"
|
||||
await ws.send(b"pong")
|
||||
|
||||
with pytest.raises(WebSocketClosed):
|
||||
await ws.receive()
|
||||
|
||||
http_setup = HTTPServerSetup(URL("/http_test"), "POST", "http_test", _handle_http)
|
||||
driver.setup_http_server(http_setup)
|
||||
@@ -53,3 +68,13 @@ async def test_reverse_driver(app: App):
|
||||
async with client.websocket_connect("/ws_test") as ws:
|
||||
await ws.send_text("ping")
|
||||
assert await ws.receive_text() == "pong"
|
||||
await ws.send_bytes(b"ping")
|
||||
assert await ws.receive_bytes() == b"pong"
|
||||
|
||||
await ws.send_text("ping")
|
||||
assert await ws.receive_text() == "pong"
|
||||
|
||||
await ws.send_bytes(b"ping")
|
||||
assert await ws.receive_bytes() == b"pong"
|
||||
|
||||
await ws.close()
|
||||
|
@@ -3,7 +3,7 @@ from nonebug import App
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weather(app: App):
|
||||
async def test_weather(app: App, load_example):
|
||||
from examples.weather import weather
|
||||
from utils import make_fake_event, make_fake_message
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import os
|
||||
import pytest
|
||||
|
||||
os.environ["CONFIG_FROM_ENV"] = '{"test": "test"}'
|
||||
os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -25,6 +26,7 @@ async def test_init(nonebug_init):
|
||||
|
||||
config = get_driver().config
|
||||
assert config.config_from_env == {"test": "test"}
|
||||
assert config.config_override == "new"
|
||||
assert config.config_from_init == "init"
|
||||
assert config.common_config == "common"
|
||||
|
||||
@@ -35,11 +37,8 @@ async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear):
|
||||
from nonebot.drivers import ForwardDriver, ReverseDriver
|
||||
from nonebot import get_app, get_bot, get_asgi, get_bots, get_driver
|
||||
|
||||
try:
|
||||
with pytest.raises(ValueError):
|
||||
get_driver()
|
||||
assert False, "Driver can only be got after initialization"
|
||||
except ValueError:
|
||||
assert True
|
||||
|
||||
nonebot.init(driver="nonebot.drivers.fastapi")
|
||||
|
||||
@@ -59,11 +58,8 @@ async def test_get(monkeypatch: pytest.MonkeyPatch, nonebug_clear):
|
||||
nonebot.run("arg", kwarg="kwarg")
|
||||
assert runned
|
||||
|
||||
try:
|
||||
with pytest.raises(ValueError):
|
||||
get_bot()
|
||||
assert False
|
||||
except ValueError:
|
||||
assert True
|
||||
|
||||
monkeypatch.setattr(driver, "_clients", {"test": "test"})
|
||||
assert get_bot() == "test"
|
||||
|
@@ -157,3 +157,41 @@ async def test_run(app: App):
|
||||
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
|
||||
from plugins.matcher.matcher_expire import (
|
||||
test_temp_matcher,
|
||||
test_datetime_matcher,
|
||||
test_timedelta_matcher,
|
||||
)
|
||||
|
||||
event = make_fake_event(_type="test")()
|
||||
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, {}
|
||||
)
|
||||
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, {}
|
||||
)
|
||||
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, {}
|
||||
)
|
||||
assert test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]
|
||||
|
@@ -36,32 +36,103 @@ async def test_depend(app: App, load_plugin):
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot(app: App, load_plugin):
|
||||
from nonebot.params import BotParam
|
||||
from plugins.param.param_bot import get_bot
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from plugins.param.param_bot import (
|
||||
FooBot,
|
||||
get_bot,
|
||||
not_bot,
|
||||
sub_bot,
|
||||
union_bot,
|
||||
legacy_bot,
|
||||
not_legacy_bot,
|
||||
)
|
||||
|
||||
async with app.test_dependent(get_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.pass_params(bot=bot)
|
||||
ctx.should_return(bot)
|
||||
|
||||
async with app.test_dependent(legacy_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.pass_params(bot=bot)
|
||||
ctx.should_return(bot)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async with app.test_dependent(not_legacy_bot, allow_types=[BotParam]) as ctx:
|
||||
...
|
||||
|
||||
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot(base=FooBot)
|
||||
ctx.pass_params(bot=bot)
|
||||
ctx.should_return(bot)
|
||||
|
||||
with pytest.raises(TypeMisMatch):
|
||||
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.pass_params(bot=bot)
|
||||
|
||||
async with app.test_dependent(union_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot(base=FooBot)
|
||||
ctx.pass_params(bot=bot)
|
||||
ctx.should_return(bot)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async with app.test_dependent(not_bot, allow_types=[BotParam]) as ctx:
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_event(app: App, load_plugin):
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from nonebot.params import EventParam, DependParam
|
||||
from plugins.param.param_event import (
|
||||
FooEvent,
|
||||
event,
|
||||
not_event,
|
||||
sub_event,
|
||||
event_type,
|
||||
event_to_me,
|
||||
union_event,
|
||||
legacy_event,
|
||||
event_message,
|
||||
event_plain_text,
|
||||
not_legacy_event,
|
||||
)
|
||||
|
||||
fake_message = make_fake_message()("text")
|
||||
fake_event = make_fake_event(_message=fake_message)()
|
||||
fake_fooevent = make_fake_event(_base=FooEvent)()
|
||||
|
||||
async with app.test_dependent(event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_event)
|
||||
ctx.should_return(fake_event)
|
||||
|
||||
async with app.test_dependent(legacy_event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_event)
|
||||
ctx.should_return(fake_event)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async with app.test_dependent(
|
||||
not_legacy_event, allow_types=[EventParam]
|
||||
) as ctx:
|
||||
...
|
||||
|
||||
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_fooevent)
|
||||
ctx.should_return(fake_fooevent)
|
||||
|
||||
with pytest.raises(TypeMisMatch):
|
||||
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_event)
|
||||
|
||||
async with app.test_dependent(union_event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_fooevent)
|
||||
ctx.should_return(fake_event)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async with app.test_dependent(not_event, allow_types=[EventParam]) as ctx:
|
||||
...
|
||||
|
||||
async with app.test_dependent(
|
||||
event_type, allow_types=[EventParam, DependParam]
|
||||
) as ctx:
|
||||
@@ -99,6 +170,7 @@ async def test_state(app: App, load_plugin):
|
||||
CMD_ARG_KEY,
|
||||
RAW_CMD_KEY,
|
||||
REGEX_GROUP,
|
||||
CMD_START_KEY,
|
||||
REGEX_MATCHED,
|
||||
)
|
||||
from plugins.param.param_state import (
|
||||
@@ -108,14 +180,22 @@ async def test_state(app: App, load_plugin):
|
||||
command_arg,
|
||||
raw_command,
|
||||
regex_group,
|
||||
legacy_state,
|
||||
command_start,
|
||||
regex_matched,
|
||||
not_legacy_state,
|
||||
shell_command_args,
|
||||
shell_command_argv,
|
||||
)
|
||||
|
||||
fake_message = make_fake_message()("text")
|
||||
fake_state = {
|
||||
PREFIX_KEY: {CMD_KEY: ("cmd",), RAW_CMD_KEY: "/cmd", CMD_ARG_KEY: fake_message},
|
||||
PREFIX_KEY: {
|
||||
CMD_KEY: ("cmd",),
|
||||
RAW_CMD_KEY: "/cmd",
|
||||
CMD_START_KEY: "/",
|
||||
CMD_ARG_KEY: fake_message,
|
||||
},
|
||||
SHELL_ARGV: ["-h"],
|
||||
SHELL_ARGS: {"help": True},
|
||||
REGEX_MATCHED: "[cq:test,arg=value]",
|
||||
@@ -127,6 +207,16 @@ async def test_state(app: App, load_plugin):
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state)
|
||||
|
||||
async with app.test_dependent(legacy_state, allow_types=[StateParam]) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
async with app.test_dependent(
|
||||
not_legacy_state, allow_types=[StateParam]
|
||||
) as ctx:
|
||||
...
|
||||
|
||||
async with app.test_dependent(
|
||||
command, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
@@ -145,6 +235,12 @@ async def test_state(app: App, load_plugin):
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[PREFIX_KEY][CMD_ARG_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
command_start, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(state=fake_state)
|
||||
ctx.should_return(fake_state[PREFIX_KEY][CMD_START_KEY])
|
||||
|
||||
async with app.test_dependent(
|
||||
shell_command_argv, allow_types=[StateParam, DependParam]
|
||||
) as ctx:
|
||||
|
165
tests/test_permission.py
Normal file
165
tests/test_permission.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from utils import make_fake_event
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission(app: App):
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.exception import SkippedException
|
||||
|
||||
async def falsy():
|
||||
return False
|
||||
|
||||
async def truthy():
|
||||
return True
|
||||
|
||||
async def skipped() -> bool:
|
||||
raise SkippedException
|
||||
|
||||
def _is_eq(a: Permission, b: Permission) -> bool:
|
||||
return {d.call for d in a.checkers} == {d.call for d in b.checkers}
|
||||
|
||||
assert _is_eq(Permission(truthy) | None, Permission(truthy))
|
||||
assert _is_eq(Permission(truthy) | falsy, Permission(truthy, falsy))
|
||||
assert _is_eq(Permission(truthy) | Permission(falsy), Permission(truthy, falsy))
|
||||
|
||||
assert _is_eq(None | Permission(truthy), Permission(truthy))
|
||||
assert _is_eq(truthy | Permission(falsy), Permission(truthy, falsy))
|
||||
|
||||
event = make_fake_event()()
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert await Permission(falsy)(bot, event) == False
|
||||
assert await Permission(truthy)(bot, event) == True
|
||||
assert await Permission(skipped)(bot, event) == False
|
||||
assert await Permission(truthy, falsy)(bot, event) == True
|
||||
assert await Permission(truthy, skipped)(bot, event) == True
|
||||
|
||||
|
||||
@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
|
||||
|
||||
dependent = list(MESSAGE.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, Message)
|
||||
|
||||
event = make_fake_event(_type=type)()
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@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
|
||||
|
||||
dependent = list(NOTICE.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, Notice)
|
||||
|
||||
event = make_fake_event(_type=type)()
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@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
|
||||
|
||||
dependent = list(REQUEST.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, Request)
|
||||
|
||||
event = make_fake_event(_type=type)()
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@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
|
||||
|
||||
dependent = list(METAEVENT.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, MetaEvent)
|
||||
|
||||
event = make_fake_event(_type=type)()
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"type,user_id,expected",
|
||||
[
|
||||
("message", "test", True),
|
||||
("message", "foo", False),
|
||||
("message", "faketest", True),
|
||||
("notice", "test", False),
|
||||
],
|
||||
)
|
||||
async def test_startswith(
|
||||
app: App,
|
||||
type: str,
|
||||
user_id: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.permission import SUPERUSER, SuperUser
|
||||
|
||||
dependent = list(SUPERUSER.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, SuperUser)
|
||||
|
||||
event = make_fake_event(_type=type, _user_id=user_id)()
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert await dependent(bot=bot, event=event) == expected
|
39
tests/test_plugin/test_get.py
Normal file
39
tests/test_plugin/test_get.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
|
||||
# check simple plugin
|
||||
plugin = nonebot.get_plugin("export")
|
||||
assert plugin
|
||||
assert plugin.module_name == "plugins.export"
|
||||
|
||||
# check sub plugin
|
||||
plugin = nonebot.get_plugin("nested_subplugin")
|
||||
assert plugin
|
||||
assert plugin.module_name == "plugins.nested.plugins.nested_subplugin"
|
||||
|
||||
# check get plugin by module name
|
||||
plugin = nonebot.get_plugin_by_module_name("plugins.nested.utils")
|
||||
assert plugin
|
||||
assert plugin.module_name == "plugins.nested"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_available_plugin(app: App):
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
|
||||
_managers.append(PluginManager(["plugins.export", "plugin.require"]))
|
||||
|
||||
# check get available plugins
|
||||
plugin_names = nonebot.get_available_plugin_names()
|
||||
assert plugin_names == {"export", "require"}
|
@@ -1,4 +1,5 @@
|
||||
import sys
|
||||
from dataclasses import asdict
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
import pytest
|
||||
@@ -9,27 +10,56 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugin(load_plugin: Set["Plugin"]):
|
||||
async def test_load_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager
|
||||
|
||||
loaded_plugins = set(
|
||||
loaded_plugins = {
|
||||
plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin
|
||||
)
|
||||
}
|
||||
assert loaded_plugins == load_plugin
|
||||
plugin = nonebot.get_plugin("export")
|
||||
assert plugin
|
||||
assert plugin.module_name == "plugins.export"
|
||||
|
||||
# check simple plugin
|
||||
assert "plugins.export" in sys.modules
|
||||
|
||||
try:
|
||||
nonebot.load_plugin("plugins.export")
|
||||
assert False
|
||||
except RuntimeError:
|
||||
assert True
|
||||
# check sub plugin
|
||||
plugin = nonebot.get_plugin("nested_subplugin")
|
||||
assert plugin
|
||||
assert "plugins.nested.plugins.nested_subplugin" in sys.modules
|
||||
assert plugin.parent_plugin == nonebot.get_plugin("nested")
|
||||
|
||||
# check load again
|
||||
with pytest.raises(RuntimeError):
|
||||
PluginManager(plugins=["plugins.export"]).load_all_plugins()
|
||||
with pytest.raises(RuntimeError):
|
||||
PluginManager(search_path=["plugins"]).load_all_plugins()
|
||||
|
||||
# check not found
|
||||
assert nonebot.load_plugin("some_plugin_not_exist") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_nested_plugin(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
|
||||
parent_plugin = nonebot.get_plugin("nested")
|
||||
sub_plugin = nonebot.get_plugin("nested_subplugin")
|
||||
sub_plugin2 = nonebot.get_plugin("nested_subplugin2")
|
||||
assert parent_plugin and sub_plugin and sub_plugin2
|
||||
assert sub_plugin.parent_plugin is parent_plugin
|
||||
assert sub_plugin2.parent_plugin is parent_plugin
|
||||
assert parent_plugin.sub_plugins == {sub_plugin, sub_plugin2}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bad_plugin(app: App):
|
||||
import nonebot
|
||||
|
||||
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
|
||||
@@ -47,8 +77,7 @@ async def test_require_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_loaded(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
import nonebot
|
||||
from nonebot.plugin import _managers
|
||||
from nonebot.plugin.manager import PluginManager
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
|
||||
m = PluginManager(["plugins.export"])
|
||||
_managers.append(m)
|
||||
@@ -80,10 +109,23 @@ async def test_require_not_declared(app: App):
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_found(app: App):
|
||||
import nonebot
|
||||
from nonebot.plugin import _managers
|
||||
|
||||
try:
|
||||
with pytest.raises(RuntimeError):
|
||||
nonebot.require("some_plugin_not_exist")
|
||||
assert False
|
||||
except RuntimeError:
|
||||
assert True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_metadata(app: App, load_plugin: Set["Plugin"]):
|
||||
import nonebot
|
||||
from plugins.metadata import Config
|
||||
|
||||
plugin = nonebot.get_plugin("metadata")
|
||||
assert plugin
|
||||
assert plugin.metadata
|
||||
assert asdict(plugin.metadata) == {
|
||||
"name": "测试插件",
|
||||
"description": "测试插件元信息",
|
||||
"usage": "无法使用",
|
||||
"config": Config,
|
||||
"extra": {"author": "NoneBot"},
|
||||
}
|
||||
|
12
tests/test_plugin/test_manager.py
Normal file
12
tests/test_plugin/test_manager.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
|
||||
@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")
|
||||
assert module1 is module2
|
@@ -20,6 +20,16 @@ async def test_rule(app: App):
|
||||
async def skipped() -> bool:
|
||||
raise SkippedException
|
||||
|
||||
def _is_eq(a: Rule, b: Rule) -> bool:
|
||||
return {d.call for d in a.checkers} == {d.call for d in b.checkers}
|
||||
|
||||
assert _is_eq(Rule(truthy) & None, Rule(truthy))
|
||||
assert _is_eq(Rule(truthy) & falsy, Rule(truthy, falsy))
|
||||
assert _is_eq(Rule(truthy) & Rule(falsy), Rule(truthy, falsy))
|
||||
|
||||
assert _is_eq(None & Rule(truthy), Rule(truthy))
|
||||
assert _is_eq(truthy & Rule(falsy), Rule(truthy, falsy))
|
||||
|
||||
event = make_fake_event()()
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
@@ -105,6 +115,43 @@ async def test_endswith(
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"msg,ignorecase,type,text,expected",
|
||||
[
|
||||
("fullmatch", False, "message", "fullmatch", True),
|
||||
("fullmatch", False, "message", "Fullmatch", False),
|
||||
("fullmatch", True, "message", "fullmatch", True),
|
||||
("fullmatch", True, "message", "Fullmatch", True),
|
||||
("fullmatch", False, "message", "fullfoo", False),
|
||||
("fullmatch", False, "message", "_fullmatch_", False),
|
||||
(("fullmatch", "foo"), False, "message", "fullmatchfoo", False),
|
||||
("fullmatch", False, "notice", "foo", False),
|
||||
],
|
||||
)
|
||||
async def test_fullmatch(
|
||||
app: App,
|
||||
msg: Union[str, Tuple[str, ...]],
|
||||
ignorecase: bool,
|
||||
type: str,
|
||||
text: str,
|
||||
expected: bool,
|
||||
):
|
||||
from nonebot.rule import FullmatchRule, fullmatch
|
||||
|
||||
test_fullmatch = fullmatch(msg, ignorecase)
|
||||
dependent = list(test_fullmatch.checkers)[0]
|
||||
checker = dependent.call
|
||||
|
||||
assert isinstance(checker, FullmatchRule)
|
||||
assert checker.msg == {msg} if isinstance(msg, str) else {*msg}
|
||||
assert checker.ignorecase == ignorecase
|
||||
|
||||
message = make_fake_message()(text)
|
||||
event = make_fake_event(_type=type, _message=message)()
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"kws,type,text,expected",
|
||||
|
19
tests/test_utils.py
Normal file
19
tests/test_utils.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import json
|
||||
|
||||
from utils import make_fake_message
|
||||
|
||||
|
||||
def test_dataclass_encoder():
|
||||
from nonebot.utils import DataclassEncoder
|
||||
|
||||
simple = json.dumps("123", cls=DataclassEncoder)
|
||||
assert simple == '"123"'
|
||||
|
||||
Message = make_fake_message()
|
||||
MessageSegment = Message.get_segment_class()
|
||||
ms = MessageSegment.nested(Message(MessageSegment.text("text")))
|
||||
s = json.dumps(ms, cls=DataclassEncoder)
|
||||
assert (
|
||||
s
|
||||
== '{"type": "node", "data": {"content": [{"type": "text", "data": {"text": "text"}}]}}'
|
||||
)
|
@@ -32,6 +32,10 @@ def make_fake_message():
|
||||
def image(url: str):
|
||||
return FakeMessageSegment("image", {"url": url})
|
||||
|
||||
@staticmethod
|
||||
def nested(content: "FakeMessage"):
|
||||
return FakeMessageSegment("node", {"content": content})
|
||||
|
||||
def is_text(self) -> bool:
|
||||
return self.type == "text"
|
||||
|
||||
@@ -57,6 +61,7 @@ def make_fake_message():
|
||||
|
||||
|
||||
def make_fake_event(
|
||||
_base: Optional[Type["Event"]] = None,
|
||||
_type: str = "message",
|
||||
_name: str = "test",
|
||||
_description: str = "test",
|
||||
@@ -68,7 +73,7 @@ def make_fake_event(
|
||||
) -> Type["Event"]:
|
||||
from nonebot.adapters import Event
|
||||
|
||||
_Fake = create_model("_Fake", __base__=Event, **fields)
|
||||
_Fake = create_model("_Fake", __base__=_base or Event, **fields)
|
||||
|
||||
class FakeEvent(_Fake):
|
||||
def get_type(self) -> str:
|
||||
|
@@ -1,5 +1,7 @@
|
||||
---
|
||||
id: index
|
||||
sidebar_position: 0
|
||||
description: 深入了解 NoneBot2 运行机制
|
||||
slug: /advanced/
|
||||
|
||||
options:
|
||||
|
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"label": "依赖注入"
|
||||
"label": "依赖注入",
|
||||
"position": 5
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_position: 2
|
||||
description: 重载事件处理函数
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 62
|
||||
weight: 61
|
||||
category: advanced
|
||||
---
|
||||
|
||||
|
@@ -1,122 +0,0 @@
|
||||
---
|
||||
options:
|
||||
menu:
|
||||
weight: 50
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 跨插件访问
|
||||
|
||||
由于 NoneBot2 独特的插件加载机制,直接使用 Python 原生 import 机制来进行插件之间的访问时,很可能会发生奇怪或意料以外的情况。为了避免这种情况发生,您可以使用如下方法来实现跨插件访问:
|
||||
|
||||
1. 将插件间的要使用的公共代码剥离出来,作为公共文件或者文件夹,提供给插件加以调用。
|
||||
2. 使用 NoneBot2 提供的 export 和 require 机制,来实现插件间的互相调用。
|
||||
3. 在保证插件被加载的情况下,可以采用 import 来访问。
|
||||
|
||||
第一种方法比较容易理解和实现,这里不再赘述,但需要注意的是,请不要将公共文件或者公共文件夹作为**插件**被 NoneBot2 加载。
|
||||
第三种方法需要保证插件被加载,插件加载的方式可以参阅[加载插件](../tutorial/plugin/load-plugin)。
|
||||
|
||||
下面将介绍第二种方法——export 和 require 机制:
|
||||
|
||||
## 使用 export 和 require
|
||||
|
||||
现在,假定有两个插件 pluginA 和 pluginB,需要在 pluginB 中调用 pluginA 中的一个变量 `varA` 和一个函数 `funcA`。
|
||||
|
||||
在上面的条件中涉及到了两种操作:一种是在 pluginA 的导出对象操作;而另一种是在 pluginB 的导入对象操作。在 NoneBot2 中,导出对象的操作用 export 机制来实现,导入对象的操作用 require 机制来实现。下面,我们将逐一进行介绍。
|
||||
|
||||
:::warning 警告
|
||||
使用这个方法进行跨插件访问时,**需要先加载导出对象的插件,再加载导入对象的插件。**
|
||||
:::
|
||||
|
||||
### 使用 export
|
||||
|
||||
在 pluginA 中,我们调用 export 机制 导出对象。
|
||||
|
||||
在 export 机制调用前,我们需要保证导出的对象已经被定义,比如:
|
||||
|
||||
```python
|
||||
varA = "varA"
|
||||
|
||||
|
||||
def funcA():
|
||||
return "funcA"
|
||||
```
|
||||
|
||||
在确保定义之后,我们可以从 `nonebot.plugin` 导入 `export()` 方法,`export()` 方法会返回一个特殊的字典 `export`:
|
||||
|
||||
```python
|
||||
from nonebot.plugin import export
|
||||
|
||||
export=export()
|
||||
```
|
||||
|
||||
这个字典可以用来装载导出的对象,它的 key 是对象导出后的命名,value 是对象本身,我们可以直接创建新的 key - value 对导出对象:
|
||||
|
||||
```python
|
||||
export.vA = varA
|
||||
export.fA = funcA
|
||||
```
|
||||
|
||||
除此之外,也支持嵌套导出对象:
|
||||
|
||||
```python
|
||||
export.sub.vA = varA
|
||||
export.sub.fA = funcA
|
||||
```
|
||||
|
||||
特别地,对于函数对象而言,export 支持用装饰器的方法来导出,因此,我们可以这样定义 `funcA`:
|
||||
|
||||
```python
|
||||
@export.sub
|
||||
def funcA():
|
||||
return "funcA"
|
||||
```
|
||||
|
||||
或者:
|
||||
|
||||
```python
|
||||
@export
|
||||
def funcA():
|
||||
return "funcA"
|
||||
```
|
||||
|
||||
通过装饰器的方法导出函数时,命名固定为函数的命名,也就是说,上面的两个例子等同于:
|
||||
|
||||
```python
|
||||
export.sub.funcA = funcA
|
||||
|
||||
export.funcA = funcA
|
||||
```
|
||||
|
||||
这样,我们就成功导出 `varA` 和 `funcA` 对象了。
|
||||
|
||||
下面我们将介绍如何在 pluginB 中导入这些对象。
|
||||
|
||||
### 使用 require
|
||||
|
||||
在 pluginB 中,我们调用 require 机制导入对象。
|
||||
|
||||
:::warning 警告
|
||||
在导入来自其他插件的对象时,请确保导出该对象的插件在引用该对象的插件之前加载。如果该插件并未被加载,则会尝试加载,加载失败则会返回 `None`。
|
||||
:::
|
||||
|
||||
我们可以从 `nonebot.plugin` 中导入 `require()` 方法:
|
||||
|
||||
```python
|
||||
from nonebot.plugin import require
|
||||
```
|
||||
|
||||
`require()` 方法的参数是插件名,它会返回在指定插件中,用 `export()` 方法创建的字典。
|
||||
|
||||
```python
|
||||
require_A = require('pluginA')
|
||||
```
|
||||
|
||||
在之前,这个字典已经存入了 `'vA'` - `varA`、`'fA'` - `funcA` 或 `'funcA'` - `funcA` 这样的 key - value 对。因此在这里我们直接调用属性来获取导入对象:
|
||||
|
||||
```python
|
||||
varA = require_A.vA
|
||||
funcA = require_A.fA or require_A.funcA
|
||||
```
|
||||
|
||||
这样,我们就在 pluginB 中成功导入了 `varA` 和 `funcA` 对象了。
|
@@ -1,7 +1,10 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 自定义事件响应器的响应权限
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 30
|
||||
weight: 40
|
||||
category: advanced
|
||||
---
|
||||
|
||||
@@ -11,7 +14,7 @@ options:
|
||||
|
||||
## 应用
|
||||
|
||||
如同 `Rule` 一样,`Permission` 可以在[注册事件响应器](../tutorial/plugin/create-matcher.md)时添加 `permission` 参数来加以应用,这样 NoneBot2 会在事件响应时检测事件主体的权限。下面我们以 `SUPERUSER` 为例,对该机制的应用做一下介绍。
|
||||
如同 `Rule` 一样,`Permission` 可以在[定义事件响应器](../tutorial/plugin/create-matcher.md)时添加 `permission` 参数来加以应用,这样 NoneBot2 会在事件响应时检测事件主体的权限。下面我们以 `SUPERUSER` 为例,对该机制的应用做一下介绍。
|
||||
|
||||
```python
|
||||
from nonebot.permission import SUPERUSER
|
||||
@@ -42,16 +45,16 @@ async def _():
|
||||
|
||||
```python
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.onebot.v11 import GroupMessageEvent
|
||||
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 _(event: GroupMessageEvent):
|
||||
if await GROUP_ADMIN(event):
|
||||
async def _(bot: Bot, event: GroupMessageEvent):
|
||||
if await GROUP_ADMIN(bot, event):
|
||||
await matcher.send("管理员测试成功")
|
||||
elif await GROUP_OWNER(event):
|
||||
elif await GROUP_OWNER(bot, event):
|
||||
await matcher.send("群主测试成功")
|
||||
else:
|
||||
await matcher.send("群员测试成功")
|
||||
|
@@ -1,7 +1,10 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
description: 发布插件到 NoneBot2 商店
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 70
|
||||
weight: 80
|
||||
category: advanced
|
||||
---
|
||||
|
||||
|
75
website/docs/advanced/rule.md
Normal file
75
website/docs/advanced/rule.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
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,7 +1,10 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
description: 在 NoneBot2 框架中添加 Hook 函数
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 40
|
||||
weight: 50
|
||||
category: advanced
|
||||
---
|
||||
|
||||
|
@@ -1,4 +1,7 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 在 NoneBot2 中使用定时任务
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 20
|
||||
@@ -13,7 +16,7 @@ options:
|
||||
|
||||
## 从 NoneBot v1 迁移
|
||||
|
||||
`APScheduler` 作为 NoneBot v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。NoneBot2 已将 `APScheduler` 独立为 nonebot_plugin_apscheduler 插件,你可以在[商店](https://v2.nonebot.dev/store.html)中找到它。
|
||||
`APScheduler` 作为 NoneBot v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。NoneBot2 已将 `APScheduler` 独立为 nonebot_plugin_apscheduler 插件,你可以在[商店](/store)中找到它。
|
||||
|
||||
相比于 NoneBot v1,NoneBot v2 只需要安装插件并修改 `scheduler` 的导入方式即可完成迁移。
|
||||
|
||||
@@ -40,21 +43,23 @@ poetry add nonebot-plugin-apscheduler
|
||||
```
|
||||
|
||||
:::tip 提示
|
||||
由于稍后我们将使用 `nonebot.require()` 方法进行导入,所以无需额外的 `nonebot.load_plugin()`
|
||||
由于稍后我们将使用 `nonebot.require()` 方法进行声明依赖,所以无需额外的 `nonebot.load_plugin()`
|
||||
:::
|
||||
|
||||
## 快速上手
|
||||
|
||||
1. 在需要设置定时任务的插件中,通过 `nonebot.require` 从 nonebot_plugin_apscheduler 导入 `scheduler` 对象
|
||||
|
||||
2. 在该对象的基础上,根据 `APScheduler` 的使用方法进一步配置定时任务
|
||||
1. 在需要设置定时任务的插件中,通过 `nonebot.require` 声明插件依赖
|
||||
2. 从 nonebot_plugin_apscheduler 导入 `scheduler` 对象
|
||||
3. 在该对象的基础上,根据 `APScheduler` 的使用方法进一步配置定时任务
|
||||
|
||||
将上述步骤归纳为最小实现的代码如下:
|
||||
|
||||
```python
|
||||
```python {3,5}
|
||||
from nonebot import require
|
||||
|
||||
scheduler = require("nonebot_plugin_apscheduler").scheduler
|
||||
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):
|
||||
@@ -69,16 +74,16 @@ scheduler.add_job(run_every_day_from_program_start, "interval", days=1, id="xxx"
|
||||
|
||||
为了使插件能够实现定时任务,需要先将 `scheduler` 对象导入插件。
|
||||
|
||||
NoneBot2 提供了 `nonebot.require` 方法来实现导入其他插件的内容,此处我们使用这个方法来导入 `scheduler` 对象。
|
||||
NoneBot2 提供了 `nonebot.require` 方法来实现声明插件依赖,确保 `nonebot_plugin_apscheduler` 插件可以在使用之前被导入。声明完成即可直接通过 import 导入 `scheduler` 对象。
|
||||
|
||||
NoneBot2 使用的 `scheduler` 对象为 `AsyncScheduler` 。
|
||||
NoneBot2 使用的 `scheduler` 对象为 `AsyncIOScheduler` 。
|
||||
|
||||
> 使用该方法传入的插件本身也需要有对应实现,关于该方法的更多介绍可以参阅[这里](./export-and-require.md)
|
||||
|
||||
```python
|
||||
```python {3,5}
|
||||
from nonebot import require
|
||||
|
||||
scheduler = require("nonebot_plugin_apscheduler").scheduler
|
||||
require("nonebot_plugin_apscheduler")
|
||||
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
```
|
||||
|
||||
### 编写定时任务
|
||||
|
@@ -1,10 +0,0 @@
|
||||
---
|
||||
description: 编写单元测试
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 单元测试
|
106
website/docs/advanced/unittest/README.mdx
Normal file
106
website/docs/advanced/unittest/README.mdx
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
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
|
||||
```
|
4
website/docs/advanced/unittest/_category_.json
Normal file
4
website/docs/advanced/unittest/_category_.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "单元测试",
|
||||
"position": 6
|
||||
}
|
162
website/docs/advanced/unittest/test-adapters.md
Normal file
162
website/docs/advanced/unittest/test-adapters.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
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 # 检测返回是否与预期一致
|
||||
```
|
122
website/docs/advanced/unittest/test-matcher-operation.md
Normal file
122
website/docs/advanced/unittest/test-matcher-operation.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
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()
|
||||
```
|
159
website/docs/advanced/unittest/test-matcher.md
Normal file
159
website/docs/advanced/unittest/test-matcher.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: 测试事件响应和 API 调用
|
||||
---
|
||||
|
||||
# 测试事件响应和 API 调用
|
||||
|
||||
事件响应器通过 `Rule` 和 `Permission` 来判断当前事件是否触发事件响应器,通过 `send` 发送消息或使用 `call_api` 调用平台 API,这里我们将对上述行为进行测试。
|
||||
|
||||
## 定义预期响应行为
|
||||
|
||||
NoneBug 提供了六种定义 `Rule` 和 `Permission` 的预期行为的方法来进行测试:
|
||||
|
||||
- `should_pass_rule`
|
||||
- `should_not_pass_rule`
|
||||
- `should_ignore_rule`
|
||||
- `should_pass_permission`
|
||||
- `should_not_pass_permission`
|
||||
- `should_ignore_permission`
|
||||
|
||||
以下为示例代码
|
||||
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
<details>
|
||||
<summary>示例插件</summary>
|
||||
|
||||
```python title=example.py
|
||||
from nonebot import on_message
|
||||
|
||||
async def always_pass():
|
||||
return True
|
||||
|
||||
async def never_pass():
|
||||
return False
|
||||
|
||||
foo = on_message(always_pass)
|
||||
bar = on_message(never_pass, permission=never_pass)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
```python {12,13,19,20,27,28}
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher(app: App, load_plugins):
|
||||
from awesome_bot.plugins.example import foo, bar
|
||||
|
||||
async with app.test_matcher(foo) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_fake_event()() # 此处替换为平台事件
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_pass_rule()
|
||||
ctx.should_pass_permission()
|
||||
|
||||
async with app.test_matcher(bar) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_fake_event()() # 此处替换为平台事件
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_not_pass_rule()
|
||||
ctx.should_not_pass_permission()
|
||||
|
||||
# 如需忽略规则/权限不通过
|
||||
async with app.test_matcher(bar) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_fake_event()() # 此处替换为平台事件
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_ignore_rule()
|
||||
ctx.should_ignore_permission()
|
||||
```
|
||||
|
||||
## 定义预期 API 调用行为
|
||||
|
||||
在[事件响应器操作](../../tutorial/plugin/matcher-operation.md)和[调用平台 API](../../tutorial/call-api.md) 中,我们已经了解如何向发送消息或调用平台 `API`。接下来对 [`send`](../../tutorial/plugin/matcher-operation.md#send) 和 [`call_api`](../../api/adapters/index.md#Bot-call_api) 进行测试。
|
||||
|
||||
### should_call_send
|
||||
|
||||
定义事件响应器预期发送消息,包括使用 [`send`](../../tutorial/plugin/matcher-operation.md#send)、[`finish`](../../tutorial/plugin/matcher-operation.md#finish)、[`pause`](../../tutorial/plugin/matcher-operation.md#pause)、[`reject`](../../tutorial/plugin/matcher-operation.md#reject) 以及 [`got`](../../tutorial/plugin/create-handler.md#使用-got-装饰器) 的 prompt 等方法发送的消息。
|
||||
|
||||
`should_call_send` 需要提供四个参数:
|
||||
|
||||
- `event`:事件对象。
|
||||
- `message`:预期的消息对象,可以是`str`、[`Message`](../../api/adapters/index.md#Message) 或 [`MessageSegment`](../../api/adapters/index.md#MessageSegment)。
|
||||
- `result`:`send` 的返回值,将会返回给插件。
|
||||
- `**kwargs`:`send` 方法的额外参数。
|
||||
|
||||
<details>
|
||||
<summary>示例插件</summary>
|
||||
|
||||
```python title=example.py
|
||||
from nonebot import on_message
|
||||
|
||||
foo = on_message()
|
||||
|
||||
@foo.handle()
|
||||
async def _():
|
||||
await foo.send("test")
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
```python {12}
|
||||
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)
|
||||
```
|
||||
|
||||
### should_call_api
|
||||
|
||||
定义事件响应器预期调用机器人 API 接口,包括使用 `call_api` 或者直接使用 `bot.some_api` 的方式调用 API。
|
||||
|
||||
`should_call_api` 需要提供四个参数:
|
||||
|
||||
- `api`:API 名称。
|
||||
- `data`:预期的请求数据。
|
||||
- `result`:`call_api` 的返回值,将会返回给插件。
|
||||
- `**kwargs`:`call_api` 方法的额外参数。
|
||||
|
||||
<details>
|
||||
<summary>示例插件</summary>
|
||||
|
||||
```python
|
||||
from nonebot import on_message
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
foo = on_message()
|
||||
|
||||
|
||||
@foo.handle()
|
||||
async def _(bot: Bot):
|
||||
await bot.example_api(test="test")
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
```python {12}
|
||||
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_api("example_api", {"test": "test"}, True)
|
||||
```
|
@@ -13,11 +13,11 @@ description: 如何获取帮助
|
||||
|
||||
2. 通过 QQ 群(点击下方链接直达)
|
||||
|
||||
[](https://jq.qq.com/?_wv=1027&k=5OFifDh)
|
||||
[](https://jq.qq.com/?_wv=1027&k=5OFifDh)
|
||||
|
||||
3. 通过 QQ 频道
|
||||
|
||||
前往 QQ 频道搜索 NoneBot 点击加入
|
||||
[](https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka)
|
||||
|
||||
4. 通过 Telegram 群(点击下方链接直达)
|
||||
|
||||
|
@@ -99,6 +99,29 @@ app = nonebot.get_asgi()
|
||||
nonebot.run(app="bot:app")
|
||||
```
|
||||
|
||||
:::warning 警告
|
||||
在 Windows 平台上开启该功能有可能会造成预料之外的影响!
|
||||
|
||||
在 `Python>=3.8` 环境下开启该功能后,在 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/intro#incompatible-with-selectoreventloop-of-asyncio-on-windows)
|
||||
|
||||
如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`),
|
||||
你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能
|
||||
:::
|
||||
|
||||
##### `fastapi_reload_dirs`
|
||||
|
||||
类型:`Optional[List[str]]`
|
||||
|
@@ -95,9 +95,9 @@ services:
|
||||
|
||||
- `DOCKERHUB_USERNAME`: 你的 Docker Hub 用户名
|
||||
- `DOCKERHUB_PASSWORD`: 你的 Docker Hub PAT([创建方法](https://docs.docker.com/docker-hub/access-tokens/))
|
||||
- `DEPLOY_HOST`: 部署服务器 IP 地址
|
||||
- `DEPLOY_HOST`: 部署服务器的 SSH 地址
|
||||
- `DEPLOY_USER`: 部署服务器用户名
|
||||
- `DEPLOY_KEY`: 部署服务器私钥
|
||||
- `DEPLOY_KEY`: 部署服务器私钥 ([创建方法](https://github.com/appleboy/ssh-action#setting-up-a-ssh-key))
|
||||
- `DEPLOY_PATH`: 部署服务器上的项目路径
|
||||
|
||||
将以下文件添加至项目下的 `.github/workflows/` 目录下:
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: start deployment
|
||||
uses: bobheadxi/deployments@v0.6
|
||||
uses: bobheadxi/deployments@v1
|
||||
id: deployment
|
||||
with:
|
||||
step: start
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
docker-compose up -d
|
||||
|
||||
- name: update deployment status
|
||||
uses: bobheadxi/deployments@v0.6
|
||||
uses: bobheadxi/deployments@v0.6.2
|
||||
if: always()
|
||||
with:
|
||||
step: finish
|
||||
@@ -227,4 +227,92 @@ stdout_logfile_maxbytes=2MB
|
||||
|
||||
## 使用 PM2 部署
|
||||
|
||||
<!-- TODO -->
|
||||
:::tip 提示
|
||||
在阅读这一节的过程中, 你总是可以参照 [PM2 官方文档](https://pm2.keymetrics.io/docs/usage/quick-start/) 来得到更多的信息
|
||||
:::
|
||||
|
||||
### 安装 PM2
|
||||
|
||||
需要有 NodeJS 10+环境来运行 PM2, ~~(什么 NTR)~~
|
||||
|
||||
然后通过以下命令安装即可:
|
||||
|
||||
```shell
|
||||
npm install -g pm2
|
||||
```
|
||||
|
||||
在安装完成后, 执行以下指令, 如果得到类似的输出则说明你安装成功了 PM2:
|
||||
|
||||
```shell
|
||||
$ pm2 -V
|
||||
5.2.0
|
||||
```
|
||||
|
||||
### 在后台运行进程
|
||||
|
||||
:::tip 提示
|
||||
以下步骤要求您在您 Bot 的工作目录下执行
|
||||
|
||||
如果您使用了虚拟环境, 请确保 Bot 启动命令能在虚拟环境中正常执行
|
||||
|
||||
换言之, Bot 程序需要在当前终端环境下正常运行
|
||||
:::
|
||||
|
||||
#### 启动 Bot 进程
|
||||
|
||||
```shell
|
||||
$ pm2 start "python -m nb_cli run" # 或者直接 nb run 也行
|
||||
|
||||
[PM2] Starting /usr/bin/bash in fork_mode (1 instance)
|
||||
[PM2] Done.
|
||||
┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
|
||||
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
|
||||
├─────┼────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
|
||||
│ 0 │ nb run │ default │ N/A │ fork │ 93061 │ 0s │ 0 │ online │ 0% │ 8.3mb │ mix │ disabled │
|
||||
└─────┴────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
|
||||
```
|
||||
|
||||
此时 Bot 进程就在后台运行了, 注意到表格第一列的 ID, 它可以用来查看和控制进程的状态
|
||||
|
||||
#### 常用命令
|
||||
|
||||
更具体的用法请移步 PM2 官方文档, ~~如果想要详细示例建议直接上手试试~~
|
||||
|
||||
其中命令中的所有`<id>`应该替换为上文启动进程后返回的 ID
|
||||
|
||||
- 查看最近 150 行日志
|
||||
|
||||
- `pm2 log <id> --lines 150`
|
||||
|
||||
- 实时监控所有进程日志
|
||||
|
||||
- `pm2 monit`
|
||||
|
||||
- 展示当前 PM2 管理的所有进程
|
||||
|
||||
- `pm2 ls`
|
||||
|
||||
- 停止某个进程
|
||||
|
||||
- `pm2 stop <id>`
|
||||
|
||||
- 删除某个进程
|
||||
|
||||
- `pm2 del <id>`
|
||||
|
||||
- 重启某个进程
|
||||
|
||||
- `pm2 restart <id>`
|
||||
|
||||
- 保存当前进程列表
|
||||
|
||||
- `pm2 save`
|
||||
|
||||
- 恢复保存的进程列表
|
||||
|
||||
- `pm2 resurrect`
|
||||
|
||||
- 设置开机自动启动进程列表
|
||||
- `pm2 startup`
|
||||
- 需要执行过 `pm2 save`
|
||||
如果不是 root 用户执行, 则需要手动添加指令返回的环境变量
|
||||
|
@@ -84,7 +84,7 @@ async def handle_func():
|
||||
```python {3-5}
|
||||
matcher = on_message()
|
||||
|
||||
@matcher.got("key")
|
||||
@matcher.got("key", prompt="Key?")
|
||||
async def handle_func(key: Message = Arg()):
|
||||
# do something here
|
||||
```
|
||||
@@ -168,7 +168,7 @@ async def _(foo: str = EventType()): ...
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import EventMessage
|
||||
|
||||
async def _(foo: str = EventMessage()): ...
|
||||
async def _(foo: Message = EventMessage()): ...
|
||||
```
|
||||
|
||||
### EventPlainText
|
||||
@@ -256,6 +256,25 @@ async def _(foo: Message = CommandArg()): ...
|
||||
命令详情只能在首次接收到命令型消息时获取,如果在事件处理后续流程中获取,则会获取到不同的值。
|
||||
:::
|
||||
|
||||
### CommandStart
|
||||
|
||||
获取命令型消息命令前缀。
|
||||
|
||||
```python {8}
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import CommandStart
|
||||
|
||||
matcher = on_command("cmd")
|
||||
|
||||
@matcher.handle()
|
||||
async def _(foo: str = CommandStart()): ...
|
||||
```
|
||||
|
||||
:::tip 提示
|
||||
命令详情只能在首次接收到命令型消息时获取,如果在事件处理后续流程中获取,则会获取到不同的值。
|
||||
:::
|
||||
|
||||
### ShellCommandArgs
|
||||
|
||||
获取 shell 命令解析后的参数。
|
||||
|
@@ -41,7 +41,7 @@ async def update_type():
|
||||
|
||||
事件响应器的匹配规则是一个 `Rule` 对象,它是一系列 `checker` 的集合,当所有的 `checker` 都返回 `True` 时,才会触发该响应器。
|
||||
|
||||
规则编写方法参考[自定义规则](#自定义规则)。
|
||||
规则编写方法参考[进阶 - 自定义规则](../../advanced/rule.md)。
|
||||
|
||||
:::warning 注意
|
||||
当会话状态更新时,`rule` 会被清空,以便会话收到新事件时能够正确匹配。
|
||||
@@ -51,7 +51,7 @@ async def update_type():
|
||||
|
||||
事件响应器的触发权限是一个 `Permission` 对象,它也是一系列 `checker` 的集合,当其中一个 `checker` 返回 `True` 时,就会触发该响应器。
|
||||
|
||||
权限编写方法参考[自定义权限](#自定义权限)。
|
||||
权限编写方法参考[进阶 - 自定义权限](../../advanced/permission.md)。
|
||||
|
||||
:::warning 注意
|
||||
与 `rule` 不同的是,`permission` 不会在会话状态更新时丢失,因此 `permission` 通常用于会话的响应控制。
|
||||
@@ -122,17 +122,12 @@ matcher = on_message()
|
||||
5. `on_notice`: 创建通知事件响应器。
|
||||
6. `on_startswith`: 创建消息开头匹配事件响应器。
|
||||
7. `on_endswith`: 创建消息结尾匹配事件响应器。
|
||||
8. `on_keyword`: 创建消息关键词匹配事件响应器。
|
||||
9. `on_command`: 创建命令消息事件响应器。
|
||||
10. `on_shell_command`: 创建 shell 命令消息事件响应器。
|
||||
11. `on_regex`: 创建正则表达式匹配事件响应器。
|
||||
12. `CommandGroup`: 创建具有共同命令名称前缀的命令组。
|
||||
13. `MatcherGroup`: 创建具有共同参数的响应器组。
|
||||
8. `on_fullmatch`: 创建消息完全匹配事件响应器。
|
||||
9. `on_keyword`: 创建消息关键词匹配事件响应器。
|
||||
10. `on_command`: 创建命令消息事件响应器。
|
||||
11. `on_shell_command`: 创建 shell 命令消息事件响应器。
|
||||
12. `on_regex`: 创建正则表达式匹配事件响应器。
|
||||
13. `CommandGroup`: 创建具有共同命令名称前缀的命令组。
|
||||
14. `MatcherGroup`: 创建具有共同参数的响应器组。
|
||||
|
||||
其中,`on_metaevent` `on_message` `on_request` `on_notice` 函数都是在 `on` 的基础上添加了对应的事件类型 `type`;`on_startswith` `on_endswith` `on_keyword` `on_command` `on_shell_command` `on_regex` 函数都是在 `on_message` 的基础上添加了对应的匹配规则 `rule`。
|
||||
|
||||
## 自定义规则
|
||||
|
||||
<!-- TODO -->
|
||||
|
||||
## 自定义权限
|
||||
其中,`on_metaevent` `on_message` `on_request` `on_notice` 函数都是在 `on` 的基础上添加了对应的事件类型 `type`;`on_startswith` `on_endswith` `on_fullmatch` `on_keyword` `on_command` `on_shell_command` `on_regex` 函数都是在 `on_message` 的基础上添加了对应的匹配规则 `rule`。
|
||||
|
@@ -101,6 +101,38 @@ nonebot.load_from_toml("plugin_config.toml", encoding="utf-8")
|
||||
nonebot.load_builtin_plugin("echo")
|
||||
```
|
||||
|
||||
## 确保插件加载和跨插件访问
|
||||
|
||||
倘若 `plugin_a`, `plugin_b` 均需被加载, 且 `plugin_b` 插件需要导入 `plugin_a` 才可运行, 可以在 `plugin_b` 利用 `require` 方法来确保插件加载, 同时可以直接 `import` 导入 `plugin_a` ,进行跨插件访问。
|
||||
|
||||
```python title=plugin_b.py
|
||||
from nonebot import require
|
||||
|
||||
require('plugin_a')
|
||||
|
||||
import plugin_a
|
||||
```
|
||||
|
||||
:::danger 警告
|
||||
不用 `require` 方法也可以进行跨插件访问,但需要保证插件已加载。例如,以下两种方式均可确保插件正确加载:
|
||||
|
||||
```python title=bot.py
|
||||
import nonebot
|
||||
|
||||
# 顺序加载
|
||||
nonebot.load_plugin("plugin_a")
|
||||
nonebot.load_plugin("plugin_b")
|
||||
```
|
||||
|
||||
```python
|
||||
import nonebot
|
||||
|
||||
# 同时加载
|
||||
nonebot.load_all_plugins(["plugin_a", "plugin_b"], [])
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 嵌套插件
|
||||
|
||||
<!-- TODO -->
|
||||
|
@@ -227,7 +227,7 @@ Message(
|
||||
```
|
||||
|
||||
```python title="对消息段进行安全的拼接"
|
||||
>>> Message.template("{} {}").format(MessageSegment.image("file:///..."), "world")
|
||||
>>> Message.template("{}{}").format(MessageSegment.image("file:///..."), "world")
|
||||
Message(
|
||||
MessageSegment(type='image', data={'file': 'file:///...'}),
|
||||
MessageSegment(type='text', data={'text': 'world'})
|
||||
@@ -236,17 +236,18 @@ Message(
|
||||
|
||||
```python title="以消息对象作为模板"
|
||||
>>> Message.template(
|
||||
... MessageSegment.text('test {user_id}') + MessageSegment.face(233) +
|
||||
... MessageSegment.text('test {message}')).format_map({'user_id':123456, 'message':'hello world'}
|
||||
... )
|
||||
... MessageSegment.text('{user_id}') + MessageSegment.face(233) +
|
||||
... MessageSegment.text('{message}')
|
||||
... ).format_map({'user_id':123456, 'message':'hello world'}
|
||||
...
|
||||
Message(
|
||||
MessageSegment(type='text', data={'text': 'test 123456'}),
|
||||
MessageSegment(type='text', data={'text': '123456'}),
|
||||
MessageSegment(type='face', data={'face': 233}),
|
||||
MessageSegment(type='text', data={'text': 'test hello world'})
|
||||
MessageSegment(type='text', data={'text': 'hello world'})
|
||||
)
|
||||
```
|
||||
|
||||
```python title="使用消息段的拓展格式规格"
|
||||
```python title="使用消息段的拓展控制符"
|
||||
>>> Message.template("{link:image}").format(link='https://...')
|
||||
Message(MessageSegment(type='image', data={'file': 'https://...'}))
|
||||
```
|
||||
|
@@ -14,6 +14,10 @@ options:
|
||||
如何**安装**协议适配器请参考[安装协议适配器](../start/install-adapter.mdx)。
|
||||
:::
|
||||
|
||||
:::warning 提示
|
||||
各适配器的具体配置与说明请跳转至 [商店 - 适配器](/store) 中各适配器右上角的主页或文档进行查看。
|
||||
:::
|
||||
|
||||
## 协议适配器的功能
|
||||
|
||||
由于 NoneBot2 的跨平台特性,需要支持不同的协议,因此需要对特定的平台协议编写一个转换器。
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user