mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 03:07:07 +00:00
Compare commits
223 Commits
v2.0.0-bet
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
20
.devcontainer/Dockerfile
Normal file
20
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/codespaces-linux/.devcontainer/base.Dockerfile
|
||||
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/universal:linux
|
||||
|
||||
# ** [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
|
||||
|
||||
# [Required] Gitmoji CLI
|
||||
# Deprecated: Maybe removed once nonemoji is done
|
||||
RUN yarn global add gitmoji-cli
|
||||
ENV PATH="$PATH:/home/codespace/.yarn/bin"
|
83
.devcontainer/devcontainer.json
Normal file
83
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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.233.0/containers/codespaces-linux
|
||||
{
|
||||
"name": "NoneBot",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": ".."
|
||||
},
|
||||
"settings": {
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"python.defaultInterpreterPath": "/opt/python/latest/bin/python",
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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"
|
||||
],
|
||||
|
||||
// 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"
|
||||
],
|
||||
|
||||
// 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 -E all && 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
|
||||
|
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: >
|
||||
|
35
.github/workflows/release-drafter.yml
vendored
35
.github/workflows/release-drafter.yml
vendored
@@ -14,24 +14,47 @@ 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 }}
|
@@ -1,8 +1,8 @@
|
||||
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
|
||||
@@ -11,12 +11,12 @@ repos:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.1.0
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.5.1
|
||||
rev: v2.6.2
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, jsx, ts, tsx, markdown]
|
||||
|
@@ -22,15 +22,22 @@ 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 git hook
|
||||
yarn global add gitmoji-cli
|
||||
gitmoji -i
|
||||
```
|
||||
|
||||
### 使用 GitHub Codespaces (Dev Container)
|
||||
|
||||
使用 GitHub Codespaces 选择 `NoneBot2` 项目,然后选择 `.devcontainer/devcontainer.json` 配置即可。
|
||||
|
||||
### Commit 规范
|
||||
|
||||
请确保你的每一个 commit 都能清晰地描述其意图,一个 commit 尽量只有一个意图。
|
||||
@@ -45,7 +52,7 @@ NoneBot2 文档并没有具体的行文风格规范,但我们建议你尽量
|
||||
|
||||
如果你需要在本地预览修改后的文档,可以使用 yarn 安装文档依赖后启动 dev server,如下所示:
|
||||
|
||||
```sh
|
||||
```bash
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
@@ -66,6 +73,6 @@ NoneBot2 的代码风格遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/
|
||||
|
||||
## Git 工作流
|
||||
|
||||
`dev` 分支为 NoneBot 的开发分支,如无特殊情况请将更改提交到该分支。
|
||||
`master` 分支为 NoneBot 的开发分支,请在任何情况下都不要直接修改 `master` 分支,而是创建一个目标分支为 `nonebot:master` 的 Pull Request 来提交修改。
|
||||
|
||||
如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `dev` 分支发起 Pull Request,注意遵循先前提到的 commit message 规范创建 commit。
|
||||
如果你不是 NoneBot 团队的成员,可在 fork 本仓库后,向本仓库的 `master` 分支发起 Pull Request,注意遵循先前提到的 commit message 规范创建 commit。我们将在 code review 通过后通过 squash merge 方式将您的贡献合并到主分支。
|
||||
|
@@ -49,7 +49,10 @@ _✨ 跨平台 Python 异步机器人框架 ✨_
|
||||
</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">
|
||||
|
@@ -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>`
|
||||
@@ -273,6 +274,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
|
||||
|
@@ -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}
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`。可用于用户输入新信息。
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -9,7 +9,6 @@ from pydantic.fields import Required, Undefined, ModelField
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from nonebot.dependencies.utils import check_field_type
|
||||
from nonebot.dependencies import Param, Dependent, CustomConfig
|
||||
from nonebot.typing import T_State, T_Handler, T_DependencyCache
|
||||
from nonebot.utils import (
|
||||
@@ -160,14 +159,14 @@ class DependParam(Param):
|
||||
class _BotChecker(Param):
|
||||
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
|
||||
field: ModelField = self.extra["field"]
|
||||
try:
|
||||
return check_field_type(field, bot)
|
||||
except TypeMisMatch:
|
||||
if isinstance(bot, field.type_):
|
||||
return bot
|
||||
else:
|
||||
logger.debug(
|
||||
f"Bot type {type(bot)} not match "
|
||||
f"annotation {field._type_display()}, ignored"
|
||||
)
|
||||
raise
|
||||
raise TypeMisMatch(field, bot)
|
||||
|
||||
|
||||
class BotParam(Param):
|
||||
@@ -206,14 +205,14 @@ class BotParam(Param):
|
||||
class _EventChecker(Param):
|
||||
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
|
||||
field: ModelField = self.extra["field"]
|
||||
try:
|
||||
return check_field_type(field, event)
|
||||
except TypeMisMatch:
|
||||
if isinstance(event, field.type_):
|
||||
return event
|
||||
else:
|
||||
logger.debug(
|
||||
f"Event type {type(event)} not match "
|
||||
f"annotation {field._type_display()}, ignored"
|
||||
)
|
||||
raise
|
||||
raise TypeMisMatch(field, event)
|
||||
|
||||
|
||||
class EventParam(Param):
|
||||
|
@@ -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.")
|
||||
|
@@ -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,7 +202,7 @@ async def _run_matcher(
|
||||
|
||||
coros = list(
|
||||
map(
|
||||
lambda x: _run_coro_with_catch(
|
||||
lambda x: run_coro_with_catch(
|
||||
x(
|
||||
matcher=matcher,
|
||||
exception=exception,
|
||||
@@ -217,7 +211,8 @@ async def _run_matcher(
|
||||
state=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>`
|
||||
@@ -58,6 +59,7 @@ 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
|
||||
|
@@ -7,6 +7,8 @@ FrontMatter:
|
||||
description: nonebot.plugin.export 模块
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from . import _current_plugin
|
||||
|
||||
|
||||
@@ -51,6 +53,11 @@ class Export(dict):
|
||||
|
||||
def export() -> Export:
|
||||
"""获取当前插件的导出内容对象"""
|
||||
warnings.warn(
|
||||
"nonebot.export() is deprecated. "
|
||||
"See https://github.com/nonebot/nonebot2/issues/935.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
plugin = _current_plugin.get()
|
||||
if not plugin:
|
||||
raise RuntimeError("Export outside of the 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:
|
||||
|
@@ -21,6 +21,7 @@ from nonebot.rule import (
|
||||
command,
|
||||
keyword,
|
||||
endswith,
|
||||
fullmatch,
|
||||
startswith,
|
||||
shell_command,
|
||||
)
|
||||
@@ -237,7 +238,7 @@ 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,
|
||||
@@ -261,7 +262,7 @@ 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,
|
||||
@@ -283,6 +284,30 @@ 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: 是否为临时事件响应器(仅执行一次)
|
||||
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,
|
||||
@@ -611,6 +636,28 @@ class MatcherGroup:
|
||||
self.matchers.append(matcher)
|
||||
return matcher
|
||||
|
||||
def on_fullmatch(self, msg: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。
|
||||
|
||||
参数:
|
||||
msg: 指定消息全匹配内容
|
||||
rule: 事件响应规则
|
||||
ignorecase: 是否忽略大小写
|
||||
permission: 事件响应权限
|
||||
handlers: 事件处理函数列表
|
||||
temp: 是否为临时事件响应器(仅执行一次)
|
||||
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]:
|
||||
"""
|
||||
注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。
|
||||
|
@@ -57,7 +57,7 @@ def on_request(
|
||||
) -> 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]] = ...,
|
||||
@@ -69,7 +69,19 @@ def on_startswith(
|
||||
) -> 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 = ...,
|
||||
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]] = ...,
|
||||
@@ -261,6 +273,19 @@ class MatcherGroup:
|
||||
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 = ...,
|
||||
priority: int = ...,
|
||||
block: bool = ...,
|
||||
state: Optional[T_State] = ...,
|
||||
) -> Type[Matcher]: ...
|
||||
def on_keyword(
|
||||
self,
|
||||
keywords: Set[str],
|
||||
|
@@ -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[
|
||||
|
@@ -21,7 +21,6 @@ from typing import (
|
||||
TypeVar,
|
||||
Callable,
|
||||
Optional,
|
||||
Awaitable,
|
||||
Coroutine,
|
||||
AsyncGenerator,
|
||||
ContextManager,
|
||||
@@ -132,6 +131,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 +155,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)
|
||||
|
||||
|
||||
|
907
poetry.lock
generated
907
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.3"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
@@ -26,10 +26,10 @@ python = "^3.7.3"
|
||||
yarl = "^1.7.2"
|
||||
loguru = "^0.6.0"
|
||||
pygtrie = "^2.4.1"
|
||||
tomlkit = "^0.9.0"
|
||||
fastapi = "^0.73.0"
|
||||
tomlkit = "^0.10.0"
|
||||
fastapi = "^0.78.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"] }
|
||||
|
@@ -1,3 +1,4 @@
|
||||
LOG_LEVEL=TRACE
|
||||
NICKNAME=["test"]
|
||||
SUPERUSERS=["test", "fake:faketest"]
|
||||
CONFIG_FROM_ENV=
|
||||
|
@@ -1,5 +1,13 @@
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
|
||||
async def get_bot(b: Bot):
|
||||
async def get_bot(b: Bot) -> Bot:
|
||||
return b
|
||||
|
||||
|
||||
class SubBot(Bot):
|
||||
...
|
||||
|
||||
|
||||
async def sub_bot(b: SubBot) -> SubBot:
|
||||
return b
|
||||
|
@@ -6,6 +6,14 @@ async def event(e: Event) -> Event:
|
||||
return e
|
||||
|
||||
|
||||
class SubEvent(Event):
|
||||
...
|
||||
|
||||
|
||||
async def sub_event(e: SubEvent) -> SubEvent:
|
||||
return e
|
||||
|
||||
|
||||
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,
|
||||
@@ -30,6 +31,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:
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -35,11 +35,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 +56,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"
|
||||
|
@@ -36,19 +36,33 @@ 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 SubBot, get_bot, sub_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(sub_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot(base=SubBot)
|
||||
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)
|
||||
|
||||
|
||||
@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 (
|
||||
SubEvent,
|
||||
event,
|
||||
sub_event,
|
||||
event_type,
|
||||
event_to_me,
|
||||
event_message,
|
||||
@@ -57,11 +71,20 @@ async def test_event(app: App, load_plugin):
|
||||
|
||||
fake_message = make_fake_message()("text")
|
||||
fake_event = make_fake_event(_message=fake_message)()
|
||||
fake_subevent = make_fake_event(_base=SubEvent)()
|
||||
|
||||
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(sub_event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_subevent)
|
||||
ctx.should_return(fake_subevent)
|
||||
|
||||
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(
|
||||
event_type, allow_types=[EventParam, DependParam]
|
||||
) as ctx:
|
||||
@@ -99,6 +122,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,6 +132,7 @@ async def test_state(app: App, load_plugin):
|
||||
command_arg,
|
||||
raw_command,
|
||||
regex_group,
|
||||
command_start,
|
||||
regex_matched,
|
||||
shell_command_args,
|
||||
shell_command_argv,
|
||||
@@ -115,7 +140,12 @@ async def test_state(app: App, load_plugin):
|
||||
|
||||
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]",
|
||||
@@ -145,6 +175,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
|
@@ -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:
|
||||
|
@@ -11,7 +11,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
|
||||
|
72
website/docs/advanced/rule.md
Normal file
72
website/docs/advanced/rule.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
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
|
||||
```
|
@@ -71,7 +71,7 @@ scheduler.add_job(run_every_day_from_program_start, "interval", days=1, id="xxx"
|
||||
|
||||
NoneBot2 提供了 `nonebot.require` 方法来实现导入其他插件的内容,此处我们使用这个方法来导入 `scheduler` 对象。
|
||||
|
||||
NoneBot2 使用的 `scheduler` 对象为 `AsyncScheduler` 。
|
||||
NoneBot2 使用的 `scheduler` 对象为 `AsyncIOScheduler` 。
|
||||
|
||||
> 使用该方法传入的插件本身也需要有对应实现,关于该方法的更多介绍可以参阅[这里](./export-and-require.md)
|
||||
|
||||
|
@@ -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: 80
|
||||
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
|
||||
```
|
3
website/docs/advanced/unittest/_category_.json
Normal file
3
website/docs/advanced/unittest/_category_.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "单元测试"
|
||||
}
|
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
|
||||
```
|
||||
@@ -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`。
|
||||
|
@@ -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 的跨平台特性,需要支持不同的协议,因此需要对特定的平台协议编写一个转换器。
|
||||
|
@@ -13,7 +13,7 @@ import ModalTitle from "./ModalTitle";
|
||||
import Paginate from "./Paginate";
|
||||
import TagComponent from "./Tag";
|
||||
|
||||
export default function Adapter(): JSX.Element {
|
||||
export default function Bot(): JSX.Element {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const {
|
||||
filter,
|
||||
|
@@ -13,7 +13,7 @@ import ModalTitle from "./ModalTitle";
|
||||
import Paginate from "./Paginate";
|
||||
import TagComponent from "./Tag";
|
||||
|
||||
export default function Adapter(): JSX.Element {
|
||||
export default function Plugin(): JSX.Element {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const {
|
||||
filter,
|
||||
|
@@ -19,12 +19,12 @@ export type Obj = {
|
||||
export function filterObjs(filter: string, objs: Obj[]): Obj[] {
|
||||
return objs.filter((o) => {
|
||||
return (
|
||||
o.module_name?.indexOf(filter) != -1 ||
|
||||
o.project_link?.indexOf(filter) != -1 ||
|
||||
o.name.indexOf(filter) != -1 ||
|
||||
o.desc.indexOf(filter) != -1 ||
|
||||
o.author.indexOf(filter) != -1 ||
|
||||
o.tags.filter((t) => t.label.indexOf(filter) != -1).length > 0
|
||||
o.module_name?.includes(filter) ||
|
||||
o.project_link?.includes(filter) ||
|
||||
o.name.includes(filter) ||
|
||||
o.desc.includes(filter) ||
|
||||
o.author.includes(filter) ||
|
||||
o.tags.filter((t) => t.label.includes(filter)).length > 0
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@@ -1,10 +1,118 @@
|
||||
---
|
||||
description: Changelog
|
||||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
# 更新日志
|
||||
|
||||
## Latest Changes
|
||||
## v2.0.0-beta.3
|
||||
|
||||
### 💥 破坏性变更
|
||||
|
||||
- Fix: 添加 export 方法 Deprecation 警告 [@yanyongyu](https://github.com/yanyongyu) ([#983](https://github.com/nonebot/nonebot2/pull/983))
|
||||
- Feature: 支持 WebSocket 连接同时获取 str 或 bytes [@yanyongyu](https://github.com/yanyongyu) ([#962](https://github.com/nonebot/nonebot2/pull/962))
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
- Feature: 支持 WebSocket 连接同时获取 str 或 bytes [@yanyongyu](https://github.com/yanyongyu) ([#962](https://github.com/nonebot/nonebot2/pull/962))
|
||||
- Feature: 添加 `CommandStart` 依赖注入参数 [@MeetWq](https://github.com/MeetWq) ([#915](https://github.com/nonebot/nonebot2/pull/915))
|
||||
- Feature: 添加 Rule, Permission 反向位运算支持 [@yanyongyu](https://github.com/yanyongyu) ([#872](https://github.com/nonebot/nonebot2/pull/872))
|
||||
- Feature: 新增文本完整匹配规则 [@A-kirami](https://github.com/A-kirami) ([#797](https://github.com/nonebot/nonebot2/pull/797))
|
||||
|
||||
### 🐛 Bug 修复
|
||||
|
||||
- Fix: 修复依赖注入默认值参数在 `__eq__` 被重写时报错的问题 [@yanyongyu](https://github.com/yanyongyu) ([#971](https://github.com/nonebot/nonebot2/pull/971))
|
||||
- Fix: 修复`MessageTemplate`在没有格式化说明符时行为不正确的问题 [@mnixry](https://github.com/mnixry) ([#947](https://github.com/nonebot/nonebot2/pull/947))
|
||||
- Fix: Bot Hook 没有捕获跳过异常 [@yanyongyu](https://github.com/yanyongyu) ([#905](https://github.com/nonebot/nonebot2/pull/905))
|
||||
- Fix: 修复部分事件响应器参数类型中冗余的 Optional [@A-kirami](https://github.com/A-kirami) ([#904](https://github.com/nonebot/nonebot2/pull/904))
|
||||
- Fix: 修复 event 类型检查会对类型进行自动转换 [@yanyongyu](https://github.com/yanyongyu) ([#876](https://github.com/nonebot/nonebot2/pull/876))
|
||||
- Fix: 修复 `on_fullmatch` 返回类型错误 [@yanyongyu](https://github.com/yanyongyu) ([#815](https://github.com/nonebot/nonebot2/pull/815))
|
||||
- Fix: 修复 DataclassEncoder 嵌套 encode 的问题 [@AkiraXie](https://github.com/AkiraXie) ([#812](https://github.com/nonebot/nonebot2/pull/812))
|
||||
|
||||
### 📝 文档
|
||||
|
||||
- Docs: 修复定时任务一节中的部分拼写错误 [@Nova-Noir](https://github.com/Nova-Noir) ([#982](https://github.com/nonebot/nonebot2/pull/982))
|
||||
- Fix: 商店搜索失效 [@yanyongyu](https://github.com/yanyongyu) ([#978](https://github.com/nonebot/nonebot2/pull/978))
|
||||
- Docs: 添加 QQ 频道链接 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#961](https://github.com/nonebot/nonebot2/pull/961))
|
||||
- Docs: 添加 nonebug 单元测试文档 [@MingxuanGame](https://github.com/MingxuanGame) ([#929](https://github.com/nonebot/nonebot2/pull/929))
|
||||
- Docs: 添加 pm2 部署文档 [@evlic](https://github.com/evlic) ([#853](https://github.com/nonebot/nonebot2/pull/853))
|
||||
- Docs: 更新 GitHub Action 部署文档 [@kexue-z](https://github.com/kexue-z) ([#937](https://github.com/nonebot/nonebot2/pull/937))
|
||||
- Docs: 添加自定义匹配规则文档 [@yanyongyu](https://github.com/yanyongyu) ([#914](https://github.com/nonebot/nonebot2/pull/914))
|
||||
- Docs: 修复适配器文档内商店链接 [@yanyongyu](https://github.com/yanyongyu) ([#861](https://github.com/nonebot/nonebot2/pull/861))
|
||||
- Docs: tips for finding adapters' document link [@StarHeartHunt](https://github.com/StarHeartHunt) ([#860](https://github.com/nonebot/nonebot2/pull/860))
|
||||
- Docs: 添加对 `fastapi_reload` 在 Windows 平台额外影响的说明 [@CherryGS](https://github.com/CherryGS) ([#830](https://github.com/nonebot/nonebot2/pull/830))
|
||||
- Docs: 修复 ci/cd action 中错误的版本号 [@Bubbleioa](https://github.com/Bubbleioa) ([#819](https://github.com/nonebot/nonebot2/pull/819))
|
||||
- Docs: 减小更新日志 toc 最大显示等级 [@yanyongyu](https://github.com/yanyongyu) ([#813](https://github.com/nonebot/nonebot2/pull/813))
|
||||
- Docs: 修改议题模板中的错误链接 [@he0119](https://github.com/he0119) ([#807](https://github.com/nonebot/nonebot2/pull/807))
|
||||
- Docs: 修改消息模板文档中错误的样例 [@mnixry](https://github.com/mnixry) ([#806](https://github.com/nonebot/nonebot2/pull/806))
|
||||
- Docs: 更新贡献指南 [@yanyongyu](https://github.com/yanyongyu) ([#798](https://github.com/nonebot/nonebot2/pull/798))
|
||||
|
||||
### 💫 杂项
|
||||
|
||||
- Plugin: nonebot-plugin-chess 改名为 nonebot-plugin-boardgame [@MeetWq](https://github.com/MeetWq) ([#953](https://github.com/nonebot/nonebot2/pull/953))
|
||||
- Plugin: 网易云无损音乐下载更改 [@kitUIN](https://github.com/kitUIN) ([#924](https://github.com/nonebot/nonebot2/pull/924))
|
||||
- Docs: 移除商店中的过期插件 [@j1g5awi](https://github.com/j1g5awi) ([#902](https://github.com/nonebot/nonebot2/pull/902))
|
||||
- CI: 修复发布机器人的意外错误 [@he0119](https://github.com/he0119) ([#892](https://github.com/nonebot/nonebot2/pull/892))
|
||||
- Docs: 替换和移除部分已经失效的插件 [@MeetWq](https://github.com/MeetWq) ([#879](https://github.com/nonebot/nonebot2/pull/879))
|
||||
- Docs: 添加 netlify 标签 [@yanyongyu](https://github.com/yanyongyu) ([#816](https://github.com/nonebot/nonebot2/pull/816))
|
||||
- Fix: 修改错误的插件 PyPI 项目名称 [@Lancercmd](https://github.com/Lancercmd) ([#804](https://github.com/nonebot/nonebot2/pull/804))
|
||||
- CI: 添加更新日志自动更新 action [@yanyongyu](https://github.com/yanyongyu) ([#799](https://github.com/nonebot/nonebot2/pull/799))
|
||||
|
||||
### 🍻 插件发布
|
||||
|
||||
- Plugin: imageutils [@yanyongyu](https://github.com/yanyongyu) ([#985](https://github.com/nonebot/nonebot2/pull/985))
|
||||
- Plugin: 摸鱼日历 [@yanyongyu](https://github.com/yanyongyu) ([#980](https://github.com/nonebot/nonebot2/pull/980))
|
||||
- Plugin: 走迷宫 [@yanyongyu](https://github.com/yanyongyu) ([#977](https://github.com/nonebot/nonebot2/pull/977))
|
||||
- Plugin: 语录娱乐 [@yanyongyu](https://github.com/yanyongyu) ([#973](https://github.com/nonebot/nonebot2/pull/973))
|
||||
- Plugin: 国内新冠疫情数据查询 [@yanyongyu](https://github.com/yanyongyu) ([#975](https://github.com/nonebot/nonebot2/pull/975))
|
||||
- Plugin: nonebot_plugin_eventdone [@yanyongyu](https://github.com/yanyongyu) ([#966](https://github.com/nonebot/nonebot2/pull/966))
|
||||
- Plugin: 幻影坦克图片合成 [@yanyongyu](https://github.com/yanyongyu) ([#968](https://github.com/nonebot/nonebot2/pull/968))
|
||||
- Plugin: 合成字符画(GIF) [@yanyongyu](https://github.com/yanyongyu) ([#964](https://github.com/nonebot/nonebot2/pull/964))
|
||||
- Plugin: 国际象棋 [@yanyongyu](https://github.com/yanyongyu) ([#957](https://github.com/nonebot/nonebot2/pull/957))
|
||||
- Plugin: NoneBot2 文档搜索 [@yanyongyu](https://github.com/yanyongyu) ([#952](https://github.com/nonebot/nonebot2/pull/952))
|
||||
- Plugin: 中国象棋 [@yanyongyu](https://github.com/yanyongyu) ([#949](https://github.com/nonebot/nonebot2/pull/949))
|
||||
- Plugin: B 站视频封面提取 [@yanyongyu](https://github.com/yanyongyu) ([#946](https://github.com/nonebot/nonebot2/pull/946))
|
||||
- Plugin: 一言 [@yanyongyu](https://github.com/yanyongyu) ([#944](https://github.com/nonebot/nonebot2/pull/944))
|
||||
- Plugin: 答案之书 [@yanyongyu](https://github.com/yanyongyu) ([#942](https://github.com/nonebot/nonebot2/pull/942))
|
||||
- Plugin: 支付宝到账语音 [@yanyongyu](https://github.com/yanyongyu) ([#940](https://github.com/nonebot/nonebot2/pull/940))
|
||||
- Plugin: nonebot-plugin-dida [@yanyongyu](https://github.com/yanyongyu) ([#934](https://github.com/nonebot/nonebot2/pull/934))
|
||||
- Plugin: 随机唐可可 [@yanyongyu](https://github.com/yanyongyu) ([#931](https://github.com/nonebot/nonebot2/pull/931))
|
||||
- Plugin: splatoon2 新闻 [@yanyongyu](https://github.com/yanyongyu) ([#917](https://github.com/nonebot/nonebot2/pull/917))
|
||||
- Plugin: nonebot_plugin_draw [@yanyongyu](https://github.com/yanyongyu) ([#910](https://github.com/nonebot/nonebot2/pull/910))
|
||||
- Plugin: 扫雷游戏 [@yanyongyu](https://github.com/yanyongyu) ([#907](https://github.com/nonebot/nonebot2/pull/907))
|
||||
- Plugin: 汉兜 Handle [@yanyongyu](https://github.com/yanyongyu) ([#899](https://github.com/nonebot/nonebot2/pull/899))
|
||||
- Plugin: 多适配器帮助函数 [@yanyongyu](https://github.com/yanyongyu) ([#897](https://github.com/nonebot/nonebot2/pull/897))
|
||||
- Plugin: 语句抽象化 [@yanyongyu](https://github.com/yanyongyu) ([#894](https://github.com/nonebot/nonebot2/pull/894))
|
||||
- Plugin: 快速搜索 [@yanyongyu](https://github.com/yanyongyu) ([#889](https://github.com/nonebot/nonebot2/pull/889))
|
||||
- Plugin: wordle 猜单词 [@yanyongyu](https://github.com/yanyongyu) ([#891](https://github.com/nonebot/nonebot2/pull/891))
|
||||
- Plugin: MediaWiki 查询 [@yanyongyu](https://github.com/yanyongyu) ([#886](https://github.com/nonebot/nonebot2/pull/886))
|
||||
- Plugin: HikariSearch [@yanyongyu](https://github.com/yanyongyu) ([#884](https://github.com/nonebot/nonebot2/pull/884))
|
||||
- Plugin: 第二个 leetcode 查询插件 [@yanyongyu](https://github.com/yanyongyu) ([#882](https://github.com/nonebot/nonebot2/pull/882))
|
||||
- Plugin: 成分姬 [@yanyongyu](https://github.com/yanyongyu) ([#878](https://github.com/nonebot/nonebot2/pull/878))
|
||||
- Plugin: Arcaea 查分插件 [@yanyongyu](https://github.com/yanyongyu) ([#875](https://github.com/nonebot/nonebot2/pull/875))
|
||||
- Plugin: QQ 自动同意好友申请 [@yanyongyu](https://github.com/yanyongyu) ([#871](https://github.com/nonebot/nonebot2/pull/871))
|
||||
- Plugin: 21 点游戏插件 [@yanyongyu](https://github.com/yanyongyu) ([#865](https://github.com/nonebot/nonebot2/pull/865))
|
||||
- Plugin: 色图生成 [@yanyongyu](https://github.com/yanyongyu) ([#863](https://github.com/nonebot/nonebot2/pull/863))
|
||||
- Plugin: bilibili 通知插件 [@yanyongyu](https://github.com/yanyongyu) ([#859](https://github.com/nonebot/nonebot2/pull/859))
|
||||
- Plugin: 订阅推送管理 [@yanyongyu](https://github.com/yanyongyu) ([#855](https://github.com/nonebot/nonebot2/pull/855))
|
||||
- Plugin: 动漫新闻 [@yanyongyu](https://github.com/yanyongyu) ([#852](https://github.com/nonebot/nonebot2/pull/852))
|
||||
- Plugin: 游戏王卡查 [@yanyongyu](https://github.com/yanyongyu) ([#846](https://github.com/nonebot/nonebot2/pull/846))
|
||||
- Plugin: 二维码识别与发送 [@yanyongyu](https://github.com/yanyongyu) ([#843](https://github.com/nonebot/nonebot2/pull/843))
|
||||
- Plugin: mockingbird [@yanyongyu](https://github.com/yanyongyu) ([#841](https://github.com/nonebot/nonebot2/pull/841))
|
||||
- Plugin: QQ 自动续火花 [@yanyongyu](https://github.com/yanyongyu) ([#839](https://github.com/nonebot/nonebot2/pull/839))
|
||||
- Plugin: 每日一句 [@yanyongyu](https://github.com/yanyongyu) ([#832](https://github.com/nonebot/nonebot2/pull/832))
|
||||
- Plugin: 原神抽卡记录分析 [@yanyongyu](https://github.com/yanyongyu) ([#829](https://github.com/nonebot/nonebot2/pull/829))
|
||||
- Plugin: YetAnotherPicSearch [@yanyongyu](https://github.com/yanyongyu) ([#825](https://github.com/nonebot/nonebot2/pull/825))
|
||||
- Plugin: 60s 读世界小插件 [@yanyongyu](https://github.com/yanyongyu) ([#810](https://github.com/nonebot/nonebot2/pull/810))
|
||||
- Plugin: pixiv.net p 站查询图片 [@yanyongyu](https://github.com/yanyongyu) ([#803](https://github.com/nonebot/nonebot2/pull/803))
|
||||
|
||||
### 🍻 机器人发布
|
||||
|
||||
- Bot: 屑岛风 Bot [@yanyongyu](https://github.com/yanyongyu) ([#987](https://github.com/nonebot/nonebot2/pull/987))
|
||||
- Bot: ShigureBot [@yanyongyu](https://github.com/yanyongyu) ([#959](https://github.com/nonebot/nonebot2/pull/959))
|
||||
- Bot: Inkar Suki [@yanyongyu](https://github.com/yanyongyu) ([#955](https://github.com/nonebot/nonebot2/pull/955))
|
||||
|
||||
## v2.0.0-beta.2
|
||||
|
||||
- 修复 `receive`, `got` 在参数为空消息时依旧会反复询问
|
||||
- 修复文档商店分页显示错误
|
||||
@@ -17,7 +125,7 @@ description: Changelog
|
||||
- 调整项目结构,分离内部定义与用户接口
|
||||
- 新增 Bot 连接事件钩子 (如 `driver.on_bot_connect` ) 的依赖注入
|
||||
|
||||
## v2.0.0b1
|
||||
## v2.0.0-beta.1
|
||||
|
||||
- 新增 `MessageTemplate` 对于 `str` 普通模板的支持
|
||||
- 移除插件加载的 `NameSpace` 模式
|
||||
|
56
website/src/theme/FooterCopyright/index.tsx
Normal file
56
website/src/theme/FooterCopyright/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "@docusaurus/Link";
|
||||
import OriginCopyright from "@theme-original/FooterCopyright";
|
||||
|
||||
function FooterCopyright() {
|
||||
return (
|
||||
<>
|
||||
<OriginCopyright />
|
||||
<div>
|
||||
<p className="flex flex-col text-base opacity-60 md:items-center md:justify-center xl:text-center">
|
||||
{/* <Link to="https://www.netlify.com">
|
||||
<img
|
||||
src="https://www.netlify.com/img/global/badges/netlify-color-accent.svg"
|
||||
alt="Deploys by Netlify"
|
||||
/>
|
||||
</Link> */}
|
||||
<span className="flex items-center justify-start md:justify-center">
|
||||
Deployed by
|
||||
<Link to="https://www.netlify.com" className="ml-1 opacity-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 147 40"
|
||||
height="1rem"
|
||||
>
|
||||
<radialGradient
|
||||
id="netlify-logo"
|
||||
cy="0%"
|
||||
r="100.11%"
|
||||
gradientTransform="matrix(0 .9989 -1.152 0 .5 -.5)"
|
||||
>
|
||||
<stop offset="0" stop-color="#20c6b7" />
|
||||
<stop offset="1" stop-color="#4d9abf" />
|
||||
</radialGradient>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
fill="currentcolor"
|
||||
d="m53.37 12.978.123 2.198c1.403-1.7 3.245-2.55 5.525-2.55 3.951 0 5.962 2.268 6.032 6.804v12.568h-4.26v-12.322c0-1.207-.26-2.1-.78-2.681-.52-.58-1.371-.87-2.552-.87-1.719 0-3 .78-3.84 2.338v13.535h-4.262v-19.02h4.016zm24.378 19.372c-2.7 0-4.89-.852-6.567-2.557-1.678-1.705-2.517-3.976-2.517-6.812v-.527c0-1.898.365-3.595 1.096-5.089.73-1.494 1.757-2.657 3.078-3.49 1.321-.831 2.794-1.247 4.42-1.247 2.583 0 4.58.826 5.988 2.478 1.41 1.653 2.114 3.99 2.114 7.014v1.723h-12.4c.13 1.57.652 2.812 1.57 3.726s2.073 1.371 3.464 1.371c1.952 0 3.542-.79 4.77-2.373l2.297 2.198c-.76 1.136-1.774 2.018-3.042 2.645-1.269.627-2.692.94-4.27.94zm-.508-16.294c-1.17 0-2.113.41-2.832 1.23-.72.82-1.178 1.963-1.377 3.428h8.12v-.317c-.094-1.43-.474-2.51-1.14-3.243-.667-.732-1.59-1.098-2.771-1.098zm16.765-7.7v4.623h3.35v3.164h-3.35v10.617c0 .726.144 1.25.43 1.573.286.322.798.483 1.535.483a6.55 6.55 0 0 0 1.49-.176v3.305c-.97.27-1.905.404-2.806.404-3.273 0-4.91-1.81-4.91-5.431v-10.776h-3.124v-3.164h3.122v-4.623h4.261zm11.137 23.643h-4.262v-27h4.262zm9.172 0h-4.262v-19.02h4.262zm-4.525-23.96c0-.655.207-1.2.622-1.634.416-.433 1.009-.65 1.78-.65.772 0 1.368.217 1.79.65.42.434.63.979.63 1.635 0 .644-.21 1.18-.63 1.608-.422.428-1.018.642-1.79.642-.771 0-1.364-.214-1.78-.642-.415-.427-.622-.964-.622-1.608zm10.663 23.96v-15.857h-2.894v-3.164h2.894v-1.74c0-2.11.584-3.738 1.753-4.887 1.17-1.148 2.806-1.722 4.91-1.722.749 0 1.544.105 2.386.316l-.105 3.34a8.375 8.375 0 0 0 -1.631-.14c-2.035 0-3.052 1.048-3.052 3.146v1.687h3.858v3.164h-3.858v15.856h-4.261zm17.87-6.117 3.858-12.903h4.542l-7.54 21.903c-1.158 3.199-3.122 4.799-5.893 4.799-.62 0-1.304-.106-2.052-.317v-3.305l.807.053c1.075 0 1.885-.196 2.429-.589.543-.392.973-1.051 1.289-1.977l.613-1.635-6.664-18.932h4.595z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#netlify-logo)"
|
||||
fill-rule="nonzero"
|
||||
d="m28.589 14.135-.014-.006c-.008-.003-.016-.006-.023-.013a.11.11 0 0 1 -.028-.093l.773-4.726 3.625 3.626-3.77 1.604a.083.083 0 0 1 -.033.006h-.015c-.005-.003-.01-.007-.02-.017a1.716 1.716 0 0 0 -.495-.381zm5.258-.288 3.876 3.876c.805.806 1.208 1.208 1.355 1.674.022.069.04.138.054.209l-9.263-3.923a.728.728 0 0 0 -.015-.006c-.037-.015-.08-.032-.08-.07s.044-.056.081-.071l.012-.005zm5.127 7.003c-.2.376-.59.766-1.25 1.427l-4.37 4.369-5.652-1.177-.03-.006c-.05-.008-.103-.017-.103-.062a1.706 1.706 0 0 0 -.655-1.193c-.023-.023-.017-.059-.01-.092 0-.005 0-.01.002-.014l1.063-6.526.004-.022c.006-.05.015-.108.06-.108a1.73 1.73 0 0 0 1.16-.665c.009-.01.015-.021.027-.027.032-.015.07 0 .103.014l9.65 4.082zm-6.625 6.801-7.186 7.186 1.23-7.56.002-.01c.001-.01.003-.02.006-.029.01-.024.036-.034.061-.044l.012-.005a1.85 1.85 0 0 0 .695-.517c.024-.028.053-.055.09-.06a.09.09 0 0 1 .029 0l5.06 1.04zm-8.707 8.707-.81.81-8.955-12.942a.424.424 0 0 0 -.01-.014c-.014-.019-.029-.038-.026-.06 0-.016.011-.03.022-.042l.01-.013c.027-.04.05-.08.075-.123l.02-.035.003-.003c.014-.024.027-.047.051-.06.021-.01.05-.006.073-.001l9.921 2.046a.164.164 0 0 1 .076.033c.013.013.016.027.019.043a1.757 1.757 0 0 0 1.028 1.175c.028.014.016.045.003.078a.238.238 0 0 0 -.015.045c-.125.76-1.197 7.298-1.485 9.063zm-1.692 1.691c-.597.591-.949.904-1.347 1.03a2 2 0 0 1 -1.206 0c-.466-.148-.869-.55-1.674-1.356l-8.993-8.993 2.349-3.643c.011-.018.022-.034.04-.047.025-.018.061-.01.091 0a2.434 2.434 0 0 0 1.638-.083c.027-.01.054-.017.075.002a.19.19 0 0 1 .028.032l8.999 13.059zm-14.087-10.186-2.063-2.063 4.074-1.738a.084.084 0 0 1 .033-.007c.034 0 .054.034.072.065a2.91 2.91 0 0 0 .13.184l.013.016c.012.017.004.034-.008.05l-2.25 3.493zm-2.976-2.976-2.61-2.61c-.444-.444-.766-.766-.99-1.043l7.936 1.646a.84.84 0 0 0 .03.005c.049.008.103.017.103.063 0 .05-.059.073-.109.092l-.023.01zm-4.056-4.995a2 2 0 0 1 .09-.495c.148-.466.55-.868 1.356-1.674l3.34-3.34a2175.525 2175.525 0 0 0 4.626 6.687c.027.036.057.076.026.106-.146.161-.292.337-.395.528a.16.16 0 0 1 -.05.062c-.013.008-.027.005-.042.002h-.002l-8.949-1.877zm5.68-6.403 4.489-4.491c.423.185 1.96.834 3.333 1.414 1.04.44 1.988.84 2.286.97.03.012.057.024.07.054.008.018.004.041 0 .06a2.003 2.003 0 0 0 .523 1.828c.03.03 0 .073-.026.11l-.014.021-4.56 7.063c-.012.02-.023.037-.043.05-.024.015-.058.008-.086.001a2.274 2.274 0 0 0 -.543-.074c-.164 0-.342.03-.522.063h-.001c-.02.003-.038.007-.054-.005a.21.21 0 0 1 -.045-.051l-4.808-7.013zm5.398-5.398 5.814-5.814c.805-.805 1.208-1.208 1.674-1.355a2 2 0 0 1 1.206 0c.466.147.869.55 1.674 1.355l1.26 1.26-4.135 6.404a.155.155 0 0 1 -.041.048c-.025.017-.06.01-.09 0a2.097 2.097 0 0 0 -1.92.37c-.027.028-.067.012-.101-.003-.54-.235-4.74-2.01-5.341-2.265zm12.506-3.676 3.818 3.818-.92 5.698v.015a.135.135 0 0 1 -.008.038c-.01.02-.03.024-.05.03a1.83 1.83 0 0 0 -.548.273.154.154 0 0 0 -.02.017c-.011.012-.022.023-.04.025a.114.114 0 0 1 -.043-.007l-5.818-2.472-.011-.005c-.037-.015-.081-.033-.081-.071a2.198 2.198 0 0 0 -.31-.915c-.028-.046-.059-.094-.035-.141zm-3.932 8.606 5.454 2.31c.03.014.063.027.076.058a.106.106 0 0 1 0 .057c-.016.08-.03.171-.03.263v.153c0 .038-.039.054-.075.069l-.011.004c-.864.369-12.13 5.173-12.147 5.173s-.035 0-.052-.017c-.03-.03 0-.072.027-.11a.76.76 0 0 0 .014-.02l4.482-6.94.008-.012c.026-.042.056-.089.104-.089l.045.007c.102.014.192.027.283.027.68 0 1.31-.331 1.69-.897a.16.16 0 0 1 .034-.04c.027-.02.067-.01.098.004zm-6.246 9.185 12.28-5.237s.018 0 .035.017c.067.067.124.112.179.154l.027.017c.025.014.05.03.052.056 0 .01 0 .016-.002.025l-1.052 6.462-.004.026c-.007.05-.014.107-.061.107a1.729 1.729 0 0 0 -1.373.847l-.005.008c-.014.023-.027.045-.05.057-.021.01-.048.006-.07.001l-9.793-2.02c-.01-.002-.152-.519-.163-.52z"
|
||||
transform="translate(-.702)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</Link>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default FooterCopyright;
|
@@ -115,5 +115,42 @@
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "Inkar Suki",
|
||||
"desc": "一个十分方便的Bot,支持包括Webhook、群管、互动等一系列功能",
|
||||
"author": "HornCopper",
|
||||
"homepage": "https://github.com/HornCopper/Inkar-Suki",
|
||||
"tags": [
|
||||
{
|
||||
"label": "Minecraft",
|
||||
"color": "#d03790"
|
||||
},
|
||||
{
|
||||
"label": "GitHub",
|
||||
"color": "#374fd0"
|
||||
},
|
||||
{
|
||||
"label": "群管",
|
||||
"color": "#4ede39"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "ShigureBot",
|
||||
"desc": "给自己的综合性娱乐Bot写的插件集",
|
||||
"author": "lgc2333",
|
||||
"homepage": "https://github.com/lgc2333/ShigureBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "屑岛风Bot",
|
||||
"desc": "自家用屑Bot",
|
||||
"author": "kexue-z",
|
||||
"homepage": "https://github.com/kexue-z/Dao-bot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
}
|
||||
]
|
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
---
|
||||
description: 编写单元测试
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
category: advanced
|
||||
---
|
||||
|
||||
# 单元测试
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 376 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
@@ -11,7 +11,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
|
72
website/versioned_docs/version-2.0.0-beta.3/advanced/rule.md
Normal file
72
website/versioned_docs/version-2.0.0-beta.3/advanced/rule.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
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
|
||||
```
|
@@ -71,7 +71,7 @@ scheduler.add_job(run_every_day_from_program_start, "interval", days=1, id="xxx"
|
||||
|
||||
NoneBot2 提供了 `nonebot.require` 方法来实现导入其他插件的内容,此处我们使用这个方法来导入 `scheduler` 对象。
|
||||
|
||||
NoneBot2 使用的 `scheduler` 对象为 `AsyncScheduler` 。
|
||||
NoneBot2 使用的 `scheduler` 对象为 `AsyncIOScheduler` 。
|
||||
|
||||
> 使用该方法传入的插件本身也需要有对应实现,关于该方法的更多介绍可以参阅[这里](./export-and-require.md)
|
||||
|
@@ -0,0 +1,106 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 使用 NoneBug 测试机器人
|
||||
slug: /advanced/unittest/
|
||||
|
||||
options:
|
||||
menu:
|
||||
weight: 80
|
||||
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
|
||||
```
|
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"label": "单元测试"
|
||||
}
|
@@ -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 # 检测返回是否与预期一致
|
||||
```
|
@@ -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()
|
||||
```
|
@@ -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)
|
||||
```
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user