⬆️ auto update by pre-commit hooks (#4181)
Release Drafter / release (push) Skipped
Release Drafter / update-release-draft (push) Failing after 38s
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.10) (push) Failing after 43s
Ruff Lint / Ruff Lint (push) Successful in 57s
Pyright Lint / Pyright Lint (pydantic-v1) (push) Failing after 1m1s
Pyright Lint / Pyright Lint (pydantic-v2) (push) Failing after 1m4s
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.13) (push) Failing after 1m4s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.11) (push) Failing after 1m5s
Site Deploy / publish (push) Failing after 1m8s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.13) (push) Failing after 1m19s
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.11) (push) Failing after 1m26s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.10) (push) Failing after 1m29s
Code Coverage / Test Coverage (pydantic-v2, ubuntu-latest, 3.12) (push) Failing after 1m31s
Code Coverage / Test Coverage (pydantic-v1, ubuntu-latest, 3.12) (push) Failing after 1m33s
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.10) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.11) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.12) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v1, macos-latest, 3.13) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.10) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.11) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.12) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v1, windows-latest, 3.13) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.10) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.11) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.12) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v2, macos-latest, 3.13) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.10) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.11) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.12) (push) Canceled after 0s
Code Coverage / Test Coverage (pydantic-v2, windows-latest, 3.13) (push) Canceled after 0s

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
pre-commit-ci[bot]
2026-09-11 22:03:57 +08:00
committed by GitHub
co-authored by pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
parent 9b4772ff8f
commit 206d31a61d
86 changed files with 819 additions and 271 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ ci:
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks" autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.1 rev: v0.16.6
hooks: hooks:
- id: ruff-check - id: ruff-check
args: [--fix] args: [--fix]
+2 -2
View File
@@ -23,8 +23,8 @@ NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但
```python ```python
from nonebot.matcher import MatcherProvider from nonebot.matcher import MatcherProvider
class CustomProvider(MatcherProvider):
... class CustomProvider(MatcherProvider): ...
``` ```
## 设置存储提供者 ## 设置存储提供者
+5
View File
@@ -48,9 +48,11 @@ NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServer
from nonebot import get_driver from nonebot import get_driver
from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup
async def hello(request: Request) -> Response: async def hello(request: Request) -> Response:
return Response(200, content="Hello, world!") return Response(200, content="Hello, world!")
if isinstance((driver := get_driver()), ASGIMixin): if isinstance((driver := get_driver()), ASGIMixin):
driver.setup_http_server( driver.setup_http_server(
HTTPServerSetup( HTTPServerSetup(
@@ -78,6 +80,7 @@ if isinstance((driver := get_driver()), ASGIMixin):
from nonebot import get_driver from nonebot import get_driver
from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup
async def ws_handler(ws: WebSocket): async def ws_handler(ws: WebSocket):
await ws.accept() await ws.accept()
try: try:
@@ -92,6 +95,7 @@ async def ws_handler(ws: WebSocket):
await websocket.close() await websocket.close()
# do some cleanup # do some cleanup
if isinstance((driver := get_driver()), ASGIMixin): if isinstance((driver := get_driver()), ASGIMixin):
driver.setup_websocket_server( driver.setup_websocket_server(
WebSocketServerSetup( WebSocketServerSetup(
@@ -129,6 +133,7 @@ from fastapi import FastAPI
app: FastAPI = nonebot.get_app() app: FastAPI = nonebot.get_app()
@app.get("/api") @app.get("/api")
async def custom_api(): async def custom_api():
return {"message": "Hello, world!"} return {"message": "Hello, world!"}
+15 -1
View File
@@ -29,6 +29,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_startup @driver.on_startup
async def do_something(): async def do_something():
pass pass
@@ -43,6 +44,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_shutdown @driver.on_shutdown
async def do_something(): async def do_something():
pass pass
@@ -57,6 +59,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_bot_connect @driver.on_bot_connect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -71,6 +74,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_bot_disconnect @driver.on_bot_disconnect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -88,6 +92,7 @@ async def do_something(bot: Bot):
from nonebot.exception import IgnoredException from nonebot.exception import IgnoredException
from nonebot.message import event_preprocessor from nonebot.message import event_preprocessor
@event_preprocessor @event_preprocessor
async def do_something(event: Event): async def do_something(event: Event):
if not event.is_tome(): if not event.is_tome():
@@ -101,6 +106,7 @@ async def do_something(event: Event):
```python ```python
from nonebot.message import event_postprocessor from nonebot.message import event_postprocessor
@event_postprocessor @event_postprocessor
async def do_something(event: Event): async def do_something(event: Event):
pass pass
@@ -114,6 +120,7 @@ async def do_something(event: Event):
from nonebot.message import run_preprocessor from nonebot.message import run_preprocessor
from nonebot.exception import IgnoredException from nonebot.exception import IgnoredException
@run_preprocessor @run_preprocessor
async def do_something(event: Event, matcher: Matcher): async def do_something(event: Event, matcher: Matcher):
if not event.is_tome(): if not event.is_tome():
@@ -127,6 +134,7 @@ async def do_something(event: Event, matcher: Matcher):
```python ```python
from nonebot.message import run_postprocessor from nonebot.message import run_postprocessor
@run_postprocessor @run_postprocessor
async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]): async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]):
pass pass
@@ -140,6 +148,7 @@ async def do_something(event: Event, matcher: Matcher, exception: Optional[Excep
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
@Bot.on_calling_api @Bot.on_calling_api
async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):
if api == "send_msg": if api == "send_msg":
@@ -154,9 +163,14 @@ async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
@Bot.on_called_api @Bot.on_called_api
async def handle_api_result( async def handle_api_result(
bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any bot: Bot,
exception: Optional[Exception],
api: str,
data: Dict[str, Any],
result: Any,
): ):
if not exception and api == "send_msg": if not exception and api == "send_msg":
raise MockApiException(result={**result, "message_id": 123}) raise MockApiException(result={**result, "message_id": 123})
@@ -21,6 +21,7 @@ options:
```python {3-5} ```python {3-5}
foo = on_message() foo = on_message()
@foo.type_updater @foo.type_updater
async def _() -> str: async def _() -> str:
return "notice" return "notice"
@@ -37,6 +38,7 @@ from nonebot.permission import User
foo = on_message() foo = on_message()
@foo.permission_updater @foo.permission_updater
async def _(event: Event, matcher: Matcher) -> Permission: async def _(event: Event, matcher: Matcher) -> Permission:
return Permission(User.from_event(event, perm=matcher.permission)) return Permission(User.from_event(event, perm=matcher.permission))
@@ -49,6 +51,7 @@ from nonebot.permission import USER
foo = on_message() foo = on_message()
@foo.permission_updater @foo.permission_updater
async def _(matcher: Matcher) -> Permission: async def _(matcher: Matcher) -> Permission:
return USER("session1", "session2", perm=matcher.permission) return USER("session1", "session2", perm=matcher.permission)
+1 -1
View File
@@ -76,7 +76,7 @@ logger.add(
level=0, level=0,
diagnose=True, diagnose=True,
format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}", format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}",
filter=default_filter filter=default_filter,
) )
``` ```
+5
View File
@@ -21,6 +21,7 @@ options:
```python {4} title=weather/__init__.py ```python {4} title=weather/__init__.py
from nonebot.adapters.console import MessageEvent from nonebot.adapters.console import MessageEvent
@weather.got("location", prompt="请输入地名") @weather.got("location", prompt="请输入地名")
async def got_location(event: MessageEvent, location: str = ArgPlainText()): async def got_location(event: MessageEvent, location: str = ArgPlainText()):
await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...") await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...")
@@ -39,10 +40,12 @@ async def got_location(event: MessageEvent, location: str = ArgPlainText()):
```python {4,8} ```python {4,8}
from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent
@matcher.handle() @matcher.handle()
async def handle_private(event: PrivateMessageEvent): async def handle_private(event: PrivateMessageEvent):
await matcher.finish("私聊消息") await matcher.finish("私聊消息")
@matcher.handle() @matcher.handle()
async def handle_group(event: GroupMessageEvent): async def handle_group(event: GroupMessageEvent):
await matcher.finish("群聊消息") await matcher.finish("群聊消息")
@@ -54,10 +57,12 @@ async def handle_group(event: GroupMessageEvent):
from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.console import Bot as ConsoleBot
from nonebot.adapters.onebot.v11 import Bot as OneBot from nonebot.adapters.onebot.v11 import Bot as OneBot
@matcher.handle() @matcher.handle()
async def handle_console(bot: ConsoleBot): async def handle_console(bot: ConsoleBot):
await bot.bell() await bot.bell()
@matcher.handle() @matcher.handle()
async def handle_onebot(bot: OneBot): async def handle_onebot(bot: OneBot):
await bot.send_group_message(group_id=123123, message="OneBot") await bot.send_group_message(group_id=123123, message="OneBot")
+7
View File
@@ -27,9 +27,11 @@ from .config import Config
plugin_config = get_plugin_config(Config) plugin_config = get_plugin_config(Config)
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
weather = on_command("天气", rule=is_enable) weather = on_command("天气", rule=is_enable)
``` ```
@@ -43,12 +45,15 @@ weather = on_command("天气", rule=is_enable)
from nonebot.rule import Rule from nonebot.rule import Rule
from nonebot.adapters import Event from nonebot.adapters import Event
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
async def is_blacklisted(event: Event) -> bool: async def is_blacklisted(event: Event) -> bool:
return event.get_user_id() not in BLACKLIST return event.get_user_id() not in BLACKLIST
rule = Rule(is_enable, is_blacklisted) rule = Rule(is_enable, is_blacklisted)
weather = on_command("天气", rule=rule) weather = on_command("天气", rule=rule)
@@ -66,9 +71,11 @@ from .config import Config
plugin_config = get_plugin_config(Config) plugin_config = get_plugin_config(Config)
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
weather = on_command( weather = on_command(
"天气", "天气",
rule=to_me() & is_enable, rule=to_me() & is_enable,
+5
View File
@@ -17,6 +17,7 @@ NoneBot 中的会话状态是一个字典,可以通过类型 `T_State` 来获
```python ```python
from nonebot.typing import T_State from nonebot.typing import T_State
@matcher.got("key", prompt="请输入密码") @matcher.got("key", prompt="请输入密码")
async def _(state: T_State, key: str = ArgPlainText()): async def _(state: T_State, key: str = ArgPlainText()):
if key != "some password": if key != "some password":
@@ -34,10 +35,12 @@ async def _(state: T_State, key: str = ArgPlainText()):
```python ```python
from nonebot.typing import T_State from nonebot.typing import T_State
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
state["key"] = "value" state["key"] = "value"
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
await matcher.finish(state["key"]) await matcher.finish(state["key"])
@@ -49,10 +52,12 @@ async def _(state: T_State):
from nonebot.typing import T_State from nonebot.typing import T_State
from nonebot.adapters import MessageTemplate from nonebot.adapters import MessageTemplate
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
state["username"] = "user" state["username"] = "user"
@matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码")) @matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码"))
async def _(): async def _():
await matcher.finish(MessageTemplate("密码为 {password}")) await matcher.finish(MessageTemplate("密码为 {password}"))
+42 -11
View File
@@ -20,7 +20,7 @@ alc = Alconna(
Args["package", str], Args["package", str],
Option("-r|--requirement", Args["file", str]), Option("-r|--requirement", Args["file", str]),
Option("-i|--index-url", Args["url", str]), Option("-i|--index-url", Args["url", str]),
) ),
) )
res = alc.parse("pip install nonebot2 -i URL") res = alc.parse("pip install nonebot2 -i URL")
@@ -383,20 +383,33 @@ alc = Alconna(..., meta=CommandMeta("foo", example="bar"))
from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config
ns = Namespace("foo", prefixes=["/"]) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ ns = Namespace(
"foo", prefixes=["/"]
) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/
alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 alc = Alconna(
"pip", Subcommand("install", Args["package", str]), namespace=ns
) # 在创建Alconna时候传入命名空间以替换默认命名空间
# 可以通过with方式创建命名空间 # 可以通过with方式创建命名空间
with namespace("bar") as np1: with namespace("bar") as np1:
np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 np1.prefixes = [
"!"
] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令
np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter
np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 np1.builtin_option_name["help"] = {
"帮助",
"-h",
} # 设置此命名空间下的命令的帮助选项名称
# 你还可以使用config来管理所有命名空间并切换至任意命名空间 # 你还可以使用config来管理所有命名空间并切换至任意命名空间
config.namespaces["foo"] = ns # 将命名空间挂载到 config 上 config.namespaces["foo"] = ns # 将命名空间挂载到 config 上
alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间 alc = Alconna(
"pip",
Subcommand("install", Args["package", str]),
namespace=config.namespaces["foo"],
) # 也是同样可以切换到"foo"命名空间
``` ```
### 修改默认的命名空间 ### 修改默认的命名空间
@@ -469,10 +482,12 @@ alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"})
alc.shortcut("echo", delete=True) # 删除快捷指令 alc.shortcut("echo", delete=True) # 删除快捷指令
# 'Alconna::eval 的快捷指令: "echo" 删除成功' # 'Alconna::eval 的快捷指令: "echo" 删除成功'
@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器 @alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器
def cb(content: str): def cb(content: str):
eval(content, {}, {}) eval(content, {}, {})
alc.parse('eval print(\\"hello world\\")') alc.parse('eval print(\\"hello world\\")')
# hello world # hello world
@@ -523,7 +538,12 @@ alc.parse("eval --shortcut list")
from arclet.alconna import Alconna, Option, CommandMeta, Args from arclet.alconna import Alconna, Option, CommandMeta, Args
alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) alc = Alconna(
"test",
Args["foo", int],
Option("BAR", Args["baz", str], compact=True),
meta=CommandMeta(compact=True),
)
assert alc.parse("test123 BARabc").matched assert alc.parse("test123 BARabc").matched
``` ```
@@ -534,7 +554,9 @@ assert alc.parse("test123 BARabc").matched
from arclet.alconna import Alconna, Option, Args, append from arclet.alconna import Alconna, Option, Args, append
alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) alc = Alconna(
"gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)
)
print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content"))
# ['abc', 'def', 'xyz'] # ['abc', 'def', 'xyz']
``` ```
@@ -577,7 +599,7 @@ from arclet.alconna import Alconna, Args, Option
alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar")
alc.parse("test --comp") alc.parse("test --comp")
''' """
output output
以下是建议的输入: 以下是建议的输入:
@@ -588,7 +610,7 @@ output
* --shortcut * --shortcut
* foo * foo
* bar * bar
''' """
``` ```
## Duplication ## Duplication
@@ -600,7 +622,16 @@ output
以pip为例,其对应的 Duplication 应如下构造: 以pip为例,其对应的 Duplication 应如下构造:
```python ```python
from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count from arclet.alconna import (
Alconna,
Args,
Option,
OptionResult,
Duplication,
SubcommandStub,
Subcommand,
count,
)
class MyDup(Duplication): class MyDup(Duplication):
@@ -29,10 +29,10 @@ from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
echo = on_alconna(Alconna("echo", Args["msg", str])) echo = on_alconna(Alconna("echo", Args["msg", str]))
@echo.handle() @echo.handle()
async def echo_exit(msg: Match[str] = AlconnaMatch("msg")): async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
await echo.finish(msg.result) await echo.finish(msg.result)
``` ```
相比于 `on_alconna` `funcommand` 增加了三个参数 `name`, `prefixes``description` 相比于 `on_alconna` `funcommand` 增加了三个参数 `name`, `prefixes``description`
@@ -54,6 +54,7 @@ book = (
.build() .build()
) )
@book.handle() @book.handle()
async def _(arp: Arparma): async def _(arp: Arparma):
await book.send(str(arp.options)) await book.send(str(arp.options))
@@ -45,14 +45,11 @@ message = UniMessage(
```python ```python
from nonebot_plugin_alconna import Button, UniMessage from nonebot_plugin_alconna import Button, UniMessage
message = ( message = UniMessage.text("hello world").keyboard(
UniMessage.text("hello world")
.keyboard(
Button("link1", url="https://example.com/1"), Button("link1", url="https://example.com/1"),
Button("link2", url="https://example.com/2"), Button("link2", url="https://example.com/2"),
Button("link3", url="https://example.com/3"), Button("link3", url="https://example.com/3"),
row=3, row=3,
)
) )
``` ```
@@ -94,6 +91,7 @@ async def _():
```python ```python
from nonebot_plugin_alconna import message_recall, message_edit, message_reaction from nonebot_plugin_alconna import message_recall, message_edit, message_reaction
@matcher.handle() @matcher.handle()
async def _(): async def _():
await message_edit(UniMessage.text("hello world")) await message_edit(UniMessage.text("hello world"))
@@ -120,9 +118,9 @@ async def _():
```python ```python
from nonebot_plugin_alconna import UniMsg from nonebot_plugin_alconna import UniMsg
@matcher.handle() @matcher.handle()
async def _(msg: UniMsg): async def _(msg: UniMsg): ...
...
``` ```
然后你可以通过 `UniMessage` 的方法来处理消息. 然后你可以通过 `UniMessage` 的方法来处理消息.
@@ -182,6 +180,7 @@ async def _(msg: UniMsg):
```python ```python
from nonebot_plugin_alconna import UniMsg from nonebot_plugin_alconna import UniMsg
@matcher.handle() @matcher.handle()
async def _(msg: UniMsg): async def _(msg: UniMsg):
data: list[dict] = msg.dump() data: list[dict] = msg.dump()
@@ -193,6 +192,7 @@ async def _(msg: UniMsg):
```python ```python
from nonebot_plugin_alconna import UniMessage from nonebot_plugin_alconna import UniMessage
@matcher.handle() @matcher.handle()
async def _(): async def _():
data = [ data = [
@@ -12,9 +12,9 @@ from nonebot_plugin_alconna import Alconna, Args, Image, on_alconna
meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image])) meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image]))
@meme.handle() @meme.handle()
async def _(img: Image): async def _(img: Image): ...
...
``` ```
## 模型定义 ## 模型定义
@@ -24,6 +24,7 @@ async def _(img: Image):
```python ```python
class Segment: class Segment:
"""基类标注""" """基类标注"""
@property @property
def type(self) -> str: ... def type(self) -> str: ...
@property @property
@@ -31,29 +32,40 @@ class Segment:
@property @property
def children(self) -> list["Segment"]: ... def children(self) -> list["Segment"]: ...
class Text(Segment): class Text(Segment):
"""Text对象, 表示一类文本元素""" """Text对象, 表示一类文本元素"""
text: str text: str
styles: dict[tuple[int, int], list[str]] styles: dict[tuple[int, int], list[str]]
def cover(self, text: str): ... def cover(self, text: str): ...
def mark(self, start: Optional[int] = None, end: Optional[int] = None, *styles: str): ... def mark(
self, start: Optional[int] = None, end: Optional[int] = None, *styles: str
): ...
class At(Segment): class At(Segment):
"""At对象, 表示一类提醒某用户的元素""" """At对象, 表示一类提醒某用户的元素"""
flag: Literal["user", "role", "channel"] flag: Literal["user", "role", "channel"]
target: str target: str
display: Optional[str] display: Optional[str]
class AtAll(Segment): class AtAll(Segment):
"""AtAll对象, 表示一类提醒所有人的元素""" """AtAll对象, 表示一类提醒所有人的元素"""
here: bool here: bool
class Emoji(Segment): class Emoji(Segment):
"""Emoji对象, 表示一类表情元素""" """Emoji对象, 表示一类表情元素"""
id: str id: str
name: Optional[str] name: Optional[str]
class Media(Segment): class Media(Segment):
id: Optional[str] id: Optional[str]
url: Optional[str] url: Optional[str]
@@ -64,53 +76,72 @@ class Media(Segment):
to_url: ClassVar[Optional[MediaToUrl]] to_url: ClassVar[Optional[MediaToUrl]]
class Image(Media): class Image(Media):
"""Image对象, 表示一类图片元素""" """Image对象, 表示一类图片元素"""
width: Optional[int] width: Optional[int]
height: Optional[int] height: Optional[int]
class Audio(Media): class Audio(Media):
"""Audio对象, 表示一类音频元素""" """Audio对象, 表示一类音频元素"""
duration: Optional[float] duration: Optional[float]
class Voice(Media): class Voice(Media):
"""Voice对象, 表示一类语音元素""" """Voice对象, 表示一类语音元素"""
duration: Optional[float] duration: Optional[float]
class Video(Media): class Video(Media):
"""Video对象, 表示一类视频元素""" """Video对象, 表示一类视频元素"""
thumbnail: Optional[Image] thumbnail: Optional[Image]
duration: Optional[float] duration: Optional[float]
class File(Media): class File(Media):
"""File对象, 表示一类文件元素""" """File对象, 表示一类文件元素"""
class Reply(Segment): class Reply(Segment):
"""Reply对象,表示一类回复消息""" """Reply对象,表示一类回复消息"""
id: str id: str
"""此处不一定是消息ID,可能是其他ID,如消息序号等""" """此处不一定是消息ID,可能是其他ID,如消息序号等"""
msg: Optional[Union[Message, str]] msg: Optional[Union[Message, str]]
origin: Optional[Any] origin: Optional[Any]
class Reference(Segment): class Reference(Segment):
"""Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类""" """Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类"""
id: Optional[str] id: Optional[str]
"""此处不一定是消息ID,可能是其他ID,如消息序号等""" """此处不一定是消息ID,可能是其他ID,如消息序号等"""
children: List[Union[RefNode, CustomNode]] children: List[Union[RefNode, CustomNode]]
class Hyper(Segment): class Hyper(Segment):
"""Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等""" """Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等"""
format: Literal["xml", "json"] format: Literal["xml", "json"]
raw: Optional[str] raw: Optional[str]
content: Optional[Union[dict, list]] content: Optional[Union[dict, list]]
class Reference(Segment): class Reference(Segment):
"""Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类""" """Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类"""
id: Optional[str] id: Optional[str]
nodes: Sequence[Union[RefNode, CustomNode]] nodes: Sequence[Union[RefNode, CustomNode]]
class Button(Segment): class Button(Segment):
"""Button对象,表示一类按钮消息""" """Button对象,表示一类按钮消息"""
flag: Literal["action", "link", "input", "enter"] flag: Literal["action", "link", "input", "enter"]
""" """
- 点击 action 类型的按钮时会触发一个关于 按钮回调 事件,该事件的 button 资源会包含上述 id - 点击 action 类型的按钮时会触发一个关于 按钮回调 事件,该事件的 button 资源会包含上述 id
@@ -138,20 +169,26 @@ class Button(Segment):
- list[At]: 指定用户/身份组可操作 - list[At]: 指定用户/身份组可操作
""" """
class Keyboard(Segment): class Keyboard(Segment):
"""Keyboard对象,表示一行按钮元素""" """Keyboard对象,表示一行按钮元素"""
id: Optional[str] id: Optional[str]
"""此处一般用来表示模板id,特殊情况下可能表示例如 bot_appid 等""" """此处一般用来表示模板id,特殊情况下可能表示例如 bot_appid 等"""
buttons: Optional[list[Button]] buttons: Optional[list[Button]]
row: Optional[int] row: Optional[int]
"""当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数""" """当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数"""
class Other(Segment): class Other(Segment):
"""其他 Segment""" """其他 Segment"""
origin: MessageSegment origin: MessageSegment
class I18n(Segment): class I18n(Segment):
"""特殊的 Segment,用于 i18n 消息""" """特殊的 Segment,用于 i18n 消息"""
item_or_scope: Union[LangItem, str] item_or_scope: Union[LangItem, str]
type_: Optional[str] = None type_: Optional[str] = None
@@ -172,10 +209,14 @@ from nonebot_plugin_alconna import Args, Image, Alconna, select
from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace
# 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果 # 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果
alc1 = Alconna("make_meme", Args["name", str]["img", select(Image).first]) # 也可以使用 select(Image).nth(0) alc1 = Alconna(
"make_meme", Args["name", str]["img", select(Image).first]
) # 也可以使用 select(Image).nth(0)
# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image # 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image
alc2 = Alconna("make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]]) alc2 = Alconna(
"make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]]
)
``` ```
也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取) 也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取)
@@ -213,10 +254,13 @@ def mfbuild(builder: MessageBuilder, seg: BaseMessageSegment):
@custom_handler(MarketFace) @custom_handler(MarketFace)
async def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool): async def mfexport(
exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool
):
if exporter.get_message_type() is Message: if exporter.get_message_type() is Message:
return MessageSegment("chronocat:marketface", seg.data)(await exporter.export(seg.children, bot, fallback)) return MessageSegment("chronocat:marketface", seg.data)(
await exporter.export(seg.children, bot, fallback)
)
``` ```
具体而言,你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。 具体而言,你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。
@@ -155,7 +155,9 @@ op.create_table( # CREATE TABLE
"weather_weather", # weather_weather "weather_weather", # weather_weather
sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL, sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL,
sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL, sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL,
sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), # CONSTRAINT pk_weather_weather PRIMARY KEY (location) sa.PrimaryKeyConstraint(
"location", name=op.f("pk_weather_weather")
), # CONSTRAINT pk_weather_weather PRIMARY KEY (location)
info={"bind_key": "weather"}, info={"bind_key": "weather"},
) )
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -245,7 +247,9 @@ from nonebot.typing import T_State
@weather.got("location", prompt="请输入地名") @weather.got("location", prompt="请输入地名")
async def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()): async def _(
state: T_State, session: async_scoped_session, location: str = ArgPlainText()
):
wea = await session.get(Weather, location) wea = await session.get(Weather, location)
if not wea: if not wea:
@@ -348,13 +352,16 @@ async def _(
```python title=weather/__init__.py {5} showLineNumbers ```python title=weather/__init__.py {5} showLineNumbers
from collections.abc import Sequence from collections.abc import Sequence
@weather.handle() @weather.handle()
async def _( async def _(
weas: Sequence[Weather] = SQLDepends( weas: Sequence[Weather] = SQLDepends(
select(Weather).where(Weather.weather == Depends(extract_arg_plain_text)) select(Weather).where(Weather.weather == Depends(extract_arg_plain_text))
), ),
): ):
await weather.send(f"今天的天气是{weas[0].weather}的城市有{''.join(wea.location for wea in weas)}") await weather.send(
f"今天的天气是{weas[0].weather}的城市有{''.join(wea.location for wea in weas)}"
)
``` ```
支持的类型标注请参见 [依赖注入](dependency)。 支持的类型标注请参见 [依赖注入](dependency)。
@@ -364,6 +371,7 @@ async def _(
```python title=weather/__init__.py {5-6,10} showLineNumbers ```python title=weather/__init__.py {5-6,10} showLineNumbers
from collections.abc import Sequence from collections.abc import Sequence
class Weather(Model): class Weather(Model):
location: Mapped[str] = mapped_column(primary_key=True) location: Mapped[str] = mapped_column(primary_key=True)
weather: Mapped[str] = Depends(extract_arg_plain_text) weather: Mapped[str] = Depends(extract_arg_plain_text)
+4 -8
View File
@@ -78,8 +78,7 @@ async def html_to_pic(
img_fetch_fn: ImgFetchFn = combined_img_fetcher, img_fetch_fn: ImgFetchFn = combined_img_fetcher,
css_fetch_fn: CSSFetchFn = combined_css_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher,
urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin, urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin,
) -> bytes: ) -> bytes: ...
...
``` ```
最核心的渲染函数。 最核心的渲染函数。
@@ -107,8 +106,7 @@ async def text_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
可用于渲染多行文本。 可用于渲染多行文本。
@@ -128,8 +126,7 @@ async def md_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。 可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。
@@ -153,8 +150,7 @@ async def template_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
渲染 jinja2 模板。 渲染 jinja2 模板。
+3
View File
@@ -44,15 +44,18 @@ require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_apscheduler import scheduler
# 基于装饰器的方式 # 基于装饰器的方式
@scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2}) @scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2})
async def run_every_2_hour(arg1: int, arg2: int): async def run_every_2_hour(arg1: int, arg2: int):
pass pass
# 基于 add_job 方法的方式 # 基于 add_job 方法的方式
def run_every_day(arg1: int, arg2: int): def run_every_day(arg1: int, arg2: int):
pass pass
scheduler.add_job( scheduler.add_job(
run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2} run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2}
) )
@@ -25,6 +25,7 @@ NoneBot 中的网络通信主要包括以下几种:
```python {5,6} title=tests/test_http_server.py ```python {5,6} title=tests/test_http_server.py
from nonebug import App from nonebug import App
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_http_server(app: App): async def test_http_server(app: App):
async with app.test_server() as ctx: async with app.test_server() as ctx:
@@ -45,6 +46,7 @@ import nonebot
from nonebug import App from nonebug import App
from nonebot.adapters.fake import Adapter from nonebot.adapters.fake import Adapter
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_http_server(app: App): async def test_http_server(app: App):
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
@@ -72,6 +74,7 @@ import nonebot
from nonebug import App from nonebug import App
from nonebot.adapters.fake import Adapter from nonebot.adapters.fake import Adapter
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_ws_server(app: App): async def test_ws_server(app: App):
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
+17 -8
View File
@@ -81,6 +81,7 @@ except Exception as e:
```python title=config.py ```python title=config.py
from pydantic import BaseModel from pydantic import BaseModel
class Config(BaseModel): class Config(BaseModel):
xxx_id: str xxx_id: str
xxx_token: str xxx_token: str
@@ -102,6 +103,7 @@ from nonebot.adapters import Adapter as BaseAdapter
from .config import Config from .config import Config
class Adapter(BaseAdapter): class Adapter(BaseAdapter):
@override @override
def __init__(self, driver: Driver, **kwargs: Any): def __init__(self, driver: Driver, **kwargs: Any):
@@ -208,9 +210,10 @@ from nonebot.drivers import (
ASGIMixin, ASGIMixin,
WebSocket, WebSocket,
HTTPServerSetup, HTTPServerSetup,
WebSocketServerSetup WebSocketServerSetup,
) )
class Adapter(BaseAdapter): class Adapter(BaseAdapter):
@override @override
def __init__(self, driver: Driver, **kwargs: Any): def __init__(self, driver: Driver, **kwargs: Any):
@@ -242,7 +245,6 @@ class Adapter(BaseAdapter):
) )
self.setup_websocket_server(ws_setup) self.setup_websocket_server(ws_setup)
async def _handle_http(self, request: Request) -> Response: async def _handle_http(self, request: Request) -> Response:
"""HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response""" """HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response"""
... ...
@@ -270,8 +272,8 @@ class Adapter(BaseAdapter):
```python {7,8,11} title=adapter.py ```python {7,8,11} title=adapter.py
from .bot import Bot from .bot import Bot
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
def _handle_connect(self): def _handle_connect(self):
bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID
bot = Bot(self, self_id=bot_id) # 实例化 Bot bot = Bot(self, self_id=bot_id) # 实例化 Bot
@@ -295,8 +297,8 @@ from .bot import Bot
from .event import Event from .event import Event
from .log import log from .log import log
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
@classmethod @classmethod
def payload_to_event(cls, payload: Dict[str, Any]) -> Event: def payload_to_event(cls, payload: Dict[str, Any]) -> Event:
"""根据平台事件的特性,转换平台 payload 为具体 Event """根据平台事件的特性,转换平台 payload 为具体 Event
@@ -316,7 +318,6 @@ class Adapter(BaseAdapter):
# 也可以尝试转为基础 Event 进行处理 # 也可以尝试转为基础 Event 进行处理
return type_validate_python(Event, payload) return type_validate_python(Event, payload)
async def _forward(self, bot: Bot): async def _forward(self, bot: Bot):
payload: Dict[str, Any] # 接收到的事件数据 payload: Dict[str, Any] # 接收到的事件数据
@@ -337,8 +338,8 @@ from nonebot.drivers import Request, WebSocket
from .bot import Bot from .bot import Bot
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
@override @override
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
log("DEBUG", f"Calling API <y>{api}</y>") # 给予日志提示 log("DEBUG", f"Calling API <y>{api}</y>") # 给予日志提示
@@ -356,7 +357,6 @@ class Adapter(BaseAdapter):
# 发送请求,返回结果 # 发送请求,返回结果
return await self.driver.request(request) return await self.driver.request(request)
# 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据 # 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据
# 通过某种方式获取到 bot 对应的 websocket 对象 # 通过某种方式获取到 bot 对应的 websocket 对象
ws: WebSocket = your_get_websocket_method(bot.self_id) ws: WebSocket = your_get_websocket_method(bot.self_id)
@@ -450,8 +450,8 @@ from typing_extensions import override
from nonebot.compat import model_dump from nonebot.compat import model_dump
from nonebot.adapters import Event as BaseEvent from nonebot.adapters import Event as BaseEvent
class Event(BaseEvent):
class Event(BaseEvent):
@override @override
def get_event_name(self) -> str: def get_event_name(self) -> str:
# 返回事件的名称,用于日志打印 # 返回事件的名称,用于日志打印
@@ -488,6 +488,7 @@ class Event(BaseEvent):
```python {7,16,20,25,34,42} title=event.py ```python {7,16,20,25,34,42} title=event.py
from .message import Message from .message import Message
class HeartbeatEvent(Event): class HeartbeatEvent(Event):
"""心跳时间,通常为元事件""" """心跳时间,通常为元事件"""
@@ -495,8 +496,10 @@ class HeartbeatEvent(Event):
def get_type(self) -> str: def get_type(self) -> str:
return "meta_event" return "meta_event"
class MessageEvent(Event): class MessageEvent(Event):
"""消息事件""" """消息事件"""
message_id: str message_id: str
user_id: str user_id: str
@@ -513,8 +516,10 @@ class MessageEvent(Event):
def get_user_id(self) -> str: def get_user_id(self) -> str:
return self.user_id return self.user_id
class JoinRoomEvent(Event): class JoinRoomEvent(Event):
"""加入房间事件,通常为通知事件""" """加入房间事件,通常为通知事件"""
user_id: str user_id: str
room_id: str room_id: str
@@ -522,8 +527,10 @@ class JoinRoomEvent(Event):
def get_type(self) -> str: def get_type(self) -> str:
return "notice" return "notice"
class ApplyAddFriendEvent(Event): class ApplyAddFriendEvent(Event):
"""申请添加好友事件,通常为请求事件""" """申请添加好友事件,通常为请求事件"""
user_id: str user_id: str
@override @override
@@ -544,6 +551,7 @@ from nonebot.utils import escape_tag
from nonebot.adapters import Message as BaseMessage from nonebot.adapters import Message as BaseMessage
from nonebot.adapters import MessageSegment as BaseMessageSegment from nonebot.adapters import MessageSegment as BaseMessageSegment
class MessageSegment(BaseMessageSegment["Message"]): class MessageSegment(BaseMessageSegment["Message"]):
@classmethod @classmethod
@override @override
@@ -591,6 +599,7 @@ class Message(BaseMessage[MessageSegment]):
```python title=tests/conftest.py ```python title=tests/conftest.py
from pathlib import Path from pathlib import Path
import nonebot.adapters import nonebot.adapters
nonebot.adapters.__path__.append( # type: ignore nonebot.adapters.__path__.append( # type: ignore
str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve()) str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve())
) )
+3 -1
View File
@@ -48,7 +48,9 @@ weather = on_command("天气")
from nonebot import on_command from nonebot import on_command
from nonebot.rule import to_me from nonebot.rule import to_me
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) weather = on_command(
"天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True
)
``` ```
这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。 这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。
+2 -4
View File
@@ -174,9 +174,7 @@ message = Message(
# 索引 # 索引
message[0] == MessageSegment.text("test") message[0] == MessageSegment.text("test")
# 切片 # 切片
message[0:2] == Message( message[0:2] == Message([MessageSegment.text("test"), MessageSegment.markdown("test2")])
[MessageSegment.text("test"), MessageSegment.markdown("test2")]
)
# 类型过滤 # 类型过滤
message["markdown"] == Message( message["markdown"] == Message(
[MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")]
@@ -262,7 +260,7 @@ msg = seg.join(
MessageSegment.text("second"), MessageSegment.text("second"),
MessageSegment.text("third"), MessageSegment.text("third"),
] ]
) ),
] ]
) )
msg == Message( msg == Message(
@@ -23,8 +23,8 @@ NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但
```python ```python
from nonebot.matcher import MatcherProvider from nonebot.matcher import MatcherProvider
class CustomProvider(MatcherProvider):
... class CustomProvider(MatcherProvider): ...
``` ```
## 设置存储提供者 ## 设置存储提供者
@@ -48,9 +48,11 @@ NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServer
from nonebot import get_driver from nonebot import get_driver
from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup
async def hello(request: Request) -> Response: async def hello(request: Request) -> Response:
return Response(200, content="Hello, world!") return Response(200, content="Hello, world!")
if isinstance((driver := get_driver()), ASGIMixin): if isinstance((driver := get_driver()), ASGIMixin):
driver.setup_http_server( driver.setup_http_server(
HTTPServerSetup( HTTPServerSetup(
@@ -78,6 +80,7 @@ if isinstance((driver := get_driver()), ASGIMixin):
from nonebot import get_driver from nonebot import get_driver
from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup
async def ws_handler(ws: WebSocket): async def ws_handler(ws: WebSocket):
await ws.accept() await ws.accept()
try: try:
@@ -92,6 +95,7 @@ async def ws_handler(ws: WebSocket):
await websocket.close() await websocket.close()
# do some cleanup # do some cleanup
if isinstance((driver := get_driver()), ASGIMixin): if isinstance((driver := get_driver()), ASGIMixin):
driver.setup_websocket_server( driver.setup_websocket_server(
WebSocketServerSetup( WebSocketServerSetup(
@@ -129,6 +133,7 @@ from fastapi import FastAPI
app: FastAPI = nonebot.get_app() app: FastAPI = nonebot.get_app()
@app.get("/api") @app.get("/api")
async def custom_api(): async def custom_api():
return {"message": "Hello, world!"} return {"message": "Hello, world!"}
@@ -29,6 +29,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_startup @driver.on_startup
async def do_something(): async def do_something():
pass pass
@@ -43,6 +44,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_shutdown @driver.on_shutdown
async def do_something(): async def do_something():
pass pass
@@ -57,6 +59,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_bot_connect @driver.on_bot_connect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -71,6 +74,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_bot_disconnect @driver.on_bot_disconnect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -88,6 +92,7 @@ async def do_something(bot: Bot):
from nonebot.exception import IgnoredException from nonebot.exception import IgnoredException
from nonebot.message import event_preprocessor from nonebot.message import event_preprocessor
@event_preprocessor @event_preprocessor
async def do_something(event: Event): async def do_something(event: Event):
if not event.is_tome(): if not event.is_tome():
@@ -101,6 +106,7 @@ async def do_something(event: Event):
```python ```python
from nonebot.message import event_postprocessor from nonebot.message import event_postprocessor
@event_postprocessor @event_postprocessor
async def do_something(event: Event): async def do_something(event: Event):
pass pass
@@ -114,6 +120,7 @@ async def do_something(event: Event):
from nonebot.message import run_preprocessor from nonebot.message import run_preprocessor
from nonebot.exception import IgnoredException from nonebot.exception import IgnoredException
@run_preprocessor @run_preprocessor
async def do_something(event: Event, matcher: Matcher): async def do_something(event: Event, matcher: Matcher):
if not event.is_tome(): if not event.is_tome():
@@ -127,6 +134,7 @@ async def do_something(event: Event, matcher: Matcher):
```python ```python
from nonebot.message import run_postprocessor from nonebot.message import run_postprocessor
@run_postprocessor @run_postprocessor
async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]): async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]):
pass pass
@@ -140,6 +148,7 @@ async def do_something(event: Event, matcher: Matcher, exception: Optional[Excep
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
@Bot.on_calling_api @Bot.on_calling_api
async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):
if api == "send_msg": if api == "send_msg":
@@ -154,9 +163,14 @@ async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
@Bot.on_called_api @Bot.on_called_api
async def handle_api_result( async def handle_api_result(
bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any bot: Bot,
exception: Optional[Exception],
api: str,
data: Dict[str, Any],
result: Any,
): ):
if not exception and api == "send_msg": if not exception and api == "send_msg":
raise MockApiException(result={**result, "message_id": 123}) raise MockApiException(result={**result, "message_id": 123})
@@ -21,6 +21,7 @@ options:
```python {3-5} ```python {3-5}
foo = on_message() foo = on_message()
@foo.type_updater @foo.type_updater
async def _() -> str: async def _() -> str:
return "notice" return "notice"
@@ -37,6 +38,7 @@ from nonebot.permission import User
foo = on_message() foo = on_message()
@foo.permission_updater @foo.permission_updater
async def _(event: Event, matcher: Matcher) -> Permission: async def _(event: Event, matcher: Matcher) -> Permission:
return Permission(User.from_event(event, perm=matcher.permission)) return Permission(User.from_event(event, perm=matcher.permission))
@@ -49,6 +51,7 @@ from nonebot.permission import USER
foo = on_message() foo = on_message()
@foo.permission_updater @foo.permission_updater
async def _(matcher: Matcher) -> Permission: async def _(matcher: Matcher) -> Permission:
return USER("session1", "session2", perm=matcher.permission) return USER("session1", "session2", perm=matcher.permission)
@@ -119,6 +119,8 @@ NoneBotException
```python ```python
matcher = on_notice(block=True) matcher = on_notice(block=True)
# 或者 # 或者
@matcher.handle() @matcher.handle()
async def handler(matcher: Matcher): async def handler(matcher: Matcher):
@@ -100,6 +100,7 @@ description: nonebot 模块
```python ```python
from nonebot.adapters.console import Adapter from nonebot.adapters.console import Adapter
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
``` ```
@@ -142,17 +142,18 @@ description: nonebot.params 模块
def depend_func() -> Any: def depend_func() -> Any:
return ... return ...
def depend_gen_func(): def depend_gen_func():
try: try:
yield ... yield ...
finally: finally:
... ...
async def handler( async def handler(
param_name: Any = Depends(depend_func), param_name: Any = Depends(depend_func),
gen: Any = Depends(depend_gen_func), gen: Any = Depends(depend_gen_func),
): ): ...
...
``` ```
## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam} ## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam}
@@ -76,7 +76,7 @@ logger.add(
level=0, level=0,
diagnose=True, diagnose=True,
format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}", format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}",
filter=default_filter filter=default_filter,
) )
``` ```
@@ -21,6 +21,7 @@ options:
```python {4} title=weather/__init__.py ```python {4} title=weather/__init__.py
from nonebot.adapters.console import MessageEvent from nonebot.adapters.console import MessageEvent
@weather.got("location", prompt="请输入地名") @weather.got("location", prompt="请输入地名")
async def got_location(event: MessageEvent, location: str = ArgPlainText()): async def got_location(event: MessageEvent, location: str = ArgPlainText()):
await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...") await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...")
@@ -39,10 +40,12 @@ async def got_location(event: MessageEvent, location: str = ArgPlainText()):
```python {4,8} ```python {4,8}
from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent
@matcher.handle() @matcher.handle()
async def handle_private(event: PrivateMessageEvent): async def handle_private(event: PrivateMessageEvent):
await matcher.finish("私聊消息") await matcher.finish("私聊消息")
@matcher.handle() @matcher.handle()
async def handle_group(event: GroupMessageEvent): async def handle_group(event: GroupMessageEvent):
await matcher.finish("群聊消息") await matcher.finish("群聊消息")
@@ -54,10 +57,12 @@ async def handle_group(event: GroupMessageEvent):
from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.console import Bot as ConsoleBot
from nonebot.adapters.onebot.v11 import Bot as OneBot from nonebot.adapters.onebot.v11 import Bot as OneBot
@matcher.handle() @matcher.handle()
async def handle_console(bot: ConsoleBot): async def handle_console(bot: ConsoleBot):
await bot.bell() await bot.bell()
@matcher.handle() @matcher.handle()
async def handle_onebot(bot: OneBot): async def handle_onebot(bot: OneBot):
await bot.send_group_message(group_id=123123, message="OneBot") await bot.send_group_message(group_id=123123, message="OneBot")
@@ -27,9 +27,11 @@ from .config import Config
plugin_config = get_plugin_config(Config) plugin_config = get_plugin_config(Config)
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
weather = on_command("天气", rule=is_enable) weather = on_command("天气", rule=is_enable)
``` ```
@@ -43,12 +45,15 @@ weather = on_command("天气", rule=is_enable)
from nonebot.rule import Rule from nonebot.rule import Rule
from nonebot.adapters import Event from nonebot.adapters import Event
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
async def is_blacklisted(event: Event) -> bool: async def is_blacklisted(event: Event) -> bool:
return event.get_user_id() not in BLACKLIST return event.get_user_id() not in BLACKLIST
rule = Rule(is_enable, is_blacklisted) rule = Rule(is_enable, is_blacklisted)
weather = on_command("天气", rule=rule) weather = on_command("天气", rule=rule)
@@ -66,9 +71,11 @@ from .config import Config
plugin_config = get_plugin_config(Config) plugin_config = get_plugin_config(Config)
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
weather = on_command( weather = on_command(
"天气", "天气",
rule=to_me() & is_enable, rule=to_me() & is_enable,
@@ -17,6 +17,7 @@ NoneBot 中的会话状态是一个字典,可以通过类型 `T_State` 来获
```python ```python
from nonebot.typing import T_State from nonebot.typing import T_State
@matcher.got("key", prompt="请输入密码") @matcher.got("key", prompt="请输入密码")
async def _(state: T_State, key: str = ArgPlainText()): async def _(state: T_State, key: str = ArgPlainText()):
if key != "some password": if key != "some password":
@@ -34,10 +35,12 @@ async def _(state: T_State, key: str = ArgPlainText()):
```python ```python
from nonebot.typing import T_State from nonebot.typing import T_State
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
state["key"] = "value" state["key"] = "value"
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
await matcher.finish(state["key"]) await matcher.finish(state["key"])
@@ -49,10 +52,12 @@ async def _(state: T_State):
from nonebot.typing import T_State from nonebot.typing import T_State
from nonebot.adapters import MessageTemplate from nonebot.adapters import MessageTemplate
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
state["username"] = "user" state["username"] = "user"
@matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码")) @matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码"))
async def _(): async def _():
await matcher.finish(MessageTemplate("密码为 {password}")) await matcher.finish(MessageTemplate("密码为 {password}"))
@@ -20,7 +20,7 @@ alc = Alconna(
Args["package", str], Args["package", str],
Option("-r|--requirement", Args["file", str]), Option("-r|--requirement", Args["file", str]),
Option("-i|--index-url", Args["url", str]), Option("-i|--index-url", Args["url", str]),
) ),
) )
res = alc.parse("pip install nonebot2 -i URL") res = alc.parse("pip install nonebot2 -i URL")
@@ -383,20 +383,33 @@ alc = Alconna(..., meta=CommandMeta("foo", example="bar"))
from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config
ns = Namespace("foo", prefixes=["/"]) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ ns = Namespace(
"foo", prefixes=["/"]
) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/
alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 alc = Alconna(
"pip", Subcommand("install", Args["package", str]), namespace=ns
) # 在创建Alconna时候传入命名空间以替换默认命名空间
# 可以通过with方式创建命名空间 # 可以通过with方式创建命名空间
with namespace("bar") as np1: with namespace("bar") as np1:
np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 np1.prefixes = [
"!"
] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令
np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter
np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 np1.builtin_option_name["help"] = {
"帮助",
"-h",
} # 设置此命名空间下的命令的帮助选项名称
# 你还可以使用config来管理所有命名空间并切换至任意命名空间 # 你还可以使用config来管理所有命名空间并切换至任意命名空间
config.namespaces["foo"] = ns # 将命名空间挂载到 config 上 config.namespaces["foo"] = ns # 将命名空间挂载到 config 上
alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间 alc = Alconna(
"pip",
Subcommand("install", Args["package", str]),
namespace=config.namespaces["foo"],
) # 也是同样可以切换到"foo"命名空间
``` ```
### 修改默认的命名空间 ### 修改默认的命名空间
@@ -469,10 +482,12 @@ alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"})
alc.shortcut("echo", delete=True) # 删除快捷指令 alc.shortcut("echo", delete=True) # 删除快捷指令
# 'Alconna::eval 的快捷指令: "echo" 删除成功' # 'Alconna::eval 的快捷指令: "echo" 删除成功'
@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器 @alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器
def cb(content: str): def cb(content: str):
eval(content, {}, {}) eval(content, {}, {})
alc.parse('eval print(\\"hello world\\")') alc.parse('eval print(\\"hello world\\")')
# hello world # hello world
@@ -523,7 +538,12 @@ alc.parse("eval --shortcut list")
from arclet.alconna import Alconna, Option, CommandMeta, Args from arclet.alconna import Alconna, Option, CommandMeta, Args
alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) alc = Alconna(
"test",
Args["foo", int],
Option("BAR", Args["baz", str], compact=True),
meta=CommandMeta(compact=True),
)
assert alc.parse("test123 BARabc").matched assert alc.parse("test123 BARabc").matched
``` ```
@@ -534,7 +554,9 @@ assert alc.parse("test123 BARabc").matched
from arclet.alconna import Alconna, Option, Args, append from arclet.alconna import Alconna, Option, Args, append
alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) alc = Alconna(
"gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)
)
print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content"))
# ['abc', 'def', 'xyz'] # ['abc', 'def', 'xyz']
``` ```
@@ -577,7 +599,7 @@ from arclet.alconna import Alconna, Args, Option
alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar")
alc.parse("test --comp") alc.parse("test --comp")
''' """
output output
以下是建议的输入: 以下是建议的输入:
@@ -588,7 +610,7 @@ output
* --shortcut * --shortcut
* foo * foo
* bar * bar
''' """
``` ```
## Duplication ## Duplication
@@ -600,7 +622,16 @@ output
以pip为例,其对应的 Duplication 应如下构造: 以pip为例,其对应的 Duplication 应如下构造:
```python ```python
from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count from arclet.alconna import (
Alconna,
Args,
Option,
OptionResult,
Duplication,
SubcommandStub,
Subcommand,
count,
)
class MyDup(Duplication): class MyDup(Duplication):
@@ -29,10 +29,10 @@ from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
echo = on_alconna(Alconna("echo", Args["msg", str])) echo = on_alconna(Alconna("echo", Args["msg", str]))
@echo.handle() @echo.handle()
async def echo_exit(msg: Match[str] = AlconnaMatch("msg")): async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
await echo.finish(msg.result) await echo.finish(msg.result)
``` ```
相比于 `on_alconna` `funcommand` 增加了三个参数 `name`, `prefixes``description` 相比于 `on_alconna` `funcommand` 增加了三个参数 `name`, `prefixes``description`
@@ -54,6 +54,7 @@ book = (
.build() .build()
) )
@book.handle() @book.handle()
async def _(arp: Arparma): async def _(arp: Arparma):
await book.send(str(arp.options)) await book.send(str(arp.options))
@@ -45,14 +45,11 @@ message = UniMessage(
```python ```python
from nonebot_plugin_alconna import Button, UniMessage from nonebot_plugin_alconna import Button, UniMessage
message = ( message = UniMessage.text("hello world").keyboard(
UniMessage.text("hello world")
.keyboard(
Button("link1", url="https://example.com/1"), Button("link1", url="https://example.com/1"),
Button("link2", url="https://example.com/2"), Button("link2", url="https://example.com/2"),
Button("link3", url="https://example.com/3"), Button("link3", url="https://example.com/3"),
row=3, row=3,
)
) )
``` ```
@@ -94,6 +91,7 @@ async def _():
```python ```python
from nonebot_plugin_alconna import message_recall, message_edit, message_reaction from nonebot_plugin_alconna import message_recall, message_edit, message_reaction
@matcher.handle() @matcher.handle()
async def _(): async def _():
await message_edit(UniMessage.text("hello world")) await message_edit(UniMessage.text("hello world"))
@@ -120,9 +118,9 @@ async def _():
```python ```python
from nonebot_plugin_alconna import UniMsg from nonebot_plugin_alconna import UniMsg
@matcher.handle() @matcher.handle()
async def _(msg: UniMsg): async def _(msg: UniMsg): ...
...
``` ```
然后你可以通过 `UniMessage` 的方法来处理消息. 然后你可以通过 `UniMessage` 的方法来处理消息.
@@ -182,6 +180,7 @@ async def _(msg: UniMsg):
```python ```python
from nonebot_plugin_alconna import UniMsg from nonebot_plugin_alconna import UniMsg
@matcher.handle() @matcher.handle()
async def _(msg: UniMsg): async def _(msg: UniMsg):
data: list[dict] = msg.dump() data: list[dict] = msg.dump()
@@ -193,6 +192,7 @@ async def _(msg: UniMsg):
```python ```python
from nonebot_plugin_alconna import UniMessage from nonebot_plugin_alconna import UniMessage
@matcher.handle() @matcher.handle()
async def _(): async def _():
data = [ data = [
@@ -12,9 +12,9 @@ from nonebot_plugin_alconna import Alconna, Args, Image, on_alconna
meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image])) meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image]))
@meme.handle() @meme.handle()
async def _(img: Image): async def _(img: Image): ...
...
``` ```
## 模型定义 ## 模型定义
@@ -24,6 +24,7 @@ async def _(img: Image):
```python ```python
class Segment: class Segment:
"""基类标注""" """基类标注"""
@property @property
def type(self) -> str: ... def type(self) -> str: ...
@property @property
@@ -31,29 +32,40 @@ class Segment:
@property @property
def children(self) -> list["Segment"]: ... def children(self) -> list["Segment"]: ...
class Text(Segment): class Text(Segment):
"""Text对象, 表示一类文本元素""" """Text对象, 表示一类文本元素"""
text: str text: str
styles: dict[tuple[int, int], list[str]] styles: dict[tuple[int, int], list[str]]
def cover(self, text: str): ... def cover(self, text: str): ...
def mark(self, start: Optional[int] = None, end: Optional[int] = None, *styles: str): ... def mark(
self, start: Optional[int] = None, end: Optional[int] = None, *styles: str
): ...
class At(Segment): class At(Segment):
"""At对象, 表示一类提醒某用户的元素""" """At对象, 表示一类提醒某用户的元素"""
flag: Literal["user", "role", "channel"] flag: Literal["user", "role", "channel"]
target: str target: str
display: Optional[str] display: Optional[str]
class AtAll(Segment): class AtAll(Segment):
"""AtAll对象, 表示一类提醒所有人的元素""" """AtAll对象, 表示一类提醒所有人的元素"""
here: bool here: bool
class Emoji(Segment): class Emoji(Segment):
"""Emoji对象, 表示一类表情元素""" """Emoji对象, 表示一类表情元素"""
id: str id: str
name: Optional[str] name: Optional[str]
class Media(Segment): class Media(Segment):
id: Optional[str] id: Optional[str]
url: Optional[str] url: Optional[str]
@@ -64,53 +76,72 @@ class Media(Segment):
to_url: ClassVar[Optional[MediaToUrl]] to_url: ClassVar[Optional[MediaToUrl]]
class Image(Media): class Image(Media):
"""Image对象, 表示一类图片元素""" """Image对象, 表示一类图片元素"""
width: Optional[int] width: Optional[int]
height: Optional[int] height: Optional[int]
class Audio(Media): class Audio(Media):
"""Audio对象, 表示一类音频元素""" """Audio对象, 表示一类音频元素"""
duration: Optional[float] duration: Optional[float]
class Voice(Media): class Voice(Media):
"""Voice对象, 表示一类语音元素""" """Voice对象, 表示一类语音元素"""
duration: Optional[float] duration: Optional[float]
class Video(Media): class Video(Media):
"""Video对象, 表示一类视频元素""" """Video对象, 表示一类视频元素"""
thumbnail: Optional[Image] thumbnail: Optional[Image]
duration: Optional[float] duration: Optional[float]
class File(Media): class File(Media):
"""File对象, 表示一类文件元素""" """File对象, 表示一类文件元素"""
class Reply(Segment): class Reply(Segment):
"""Reply对象,表示一类回复消息""" """Reply对象,表示一类回复消息"""
id: str id: str
"""此处不一定是消息ID,可能是其他ID,如消息序号等""" """此处不一定是消息ID,可能是其他ID,如消息序号等"""
msg: Optional[Union[Message, str]] msg: Optional[Union[Message, str]]
origin: Optional[Any] origin: Optional[Any]
class Reference(Segment): class Reference(Segment):
"""Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类""" """Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类"""
id: Optional[str] id: Optional[str]
"""此处不一定是消息ID,可能是其他ID,如消息序号等""" """此处不一定是消息ID,可能是其他ID,如消息序号等"""
children: List[Union[RefNode, CustomNode]] children: List[Union[RefNode, CustomNode]]
class Hyper(Segment): class Hyper(Segment):
"""Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等""" """Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等"""
format: Literal["xml", "json"] format: Literal["xml", "json"]
raw: Optional[str] raw: Optional[str]
content: Optional[Union[dict, list]] content: Optional[Union[dict, list]]
class Reference(Segment): class Reference(Segment):
"""Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类""" """Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类"""
id: Optional[str] id: Optional[str]
nodes: Sequence[Union[RefNode, CustomNode]] nodes: Sequence[Union[RefNode, CustomNode]]
class Button(Segment): class Button(Segment):
"""Button对象,表示一类按钮消息""" """Button对象,表示一类按钮消息"""
flag: Literal["action", "link", "input", "enter"] flag: Literal["action", "link", "input", "enter"]
""" """
- 点击 action 类型的按钮时会触发一个关于 按钮回调 事件,该事件的 button 资源会包含上述 id - 点击 action 类型的按钮时会触发一个关于 按钮回调 事件,该事件的 button 资源会包含上述 id
@@ -138,20 +169,26 @@ class Button(Segment):
- list[At]: 指定用户/身份组可操作 - list[At]: 指定用户/身份组可操作
""" """
class Keyboard(Segment): class Keyboard(Segment):
"""Keyboard对象,表示一行按钮元素""" """Keyboard对象,表示一行按钮元素"""
id: Optional[str] id: Optional[str]
"""此处一般用来表示模板id,特殊情况下可能表示例如 bot_appid 等""" """此处一般用来表示模板id,特殊情况下可能表示例如 bot_appid 等"""
buttons: Optional[list[Button]] buttons: Optional[list[Button]]
row: Optional[int] row: Optional[int]
"""当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数""" """当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数"""
class Other(Segment): class Other(Segment):
"""其他 Segment""" """其他 Segment"""
origin: MessageSegment origin: MessageSegment
class I18n(Segment): class I18n(Segment):
"""特殊的 Segment,用于 i18n 消息""" """特殊的 Segment,用于 i18n 消息"""
item_or_scope: Union[LangItem, str] item_or_scope: Union[LangItem, str]
type_: Optional[str] = None type_: Optional[str] = None
@@ -172,10 +209,14 @@ from nonebot_plugin_alconna import Args, Image, Alconna, select
from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace
# 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果 # 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果
alc1 = Alconna("make_meme", Args["name", str]["img", select(Image).first]) # 也可以使用 select(Image).nth(0) alc1 = Alconna(
"make_meme", Args["name", str]["img", select(Image).first]
) # 也可以使用 select(Image).nth(0)
# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image # 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image
alc2 = Alconna("make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]]) alc2 = Alconna(
"make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]]
)
``` ```
也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取) 也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取)
@@ -213,10 +254,13 @@ def mfbuild(builder: MessageBuilder, seg: BaseMessageSegment):
@custom_handler(MarketFace) @custom_handler(MarketFace)
async def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool): async def mfexport(
exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool
):
if exporter.get_message_type() is Message: if exporter.get_message_type() is Message:
return MessageSegment("chronocat:marketface", seg.data)(await exporter.export(seg.children, bot, fallback)) return MessageSegment("chronocat:marketface", seg.data)(
await exporter.export(seg.children, bot, fallback)
)
``` ```
具体而言,你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。 具体而言,你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。
@@ -155,7 +155,9 @@ op.create_table( # CREATE TABLE
"weather_weather", # weather_weather "weather_weather", # weather_weather
sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL, sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL,
sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL, sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL,
sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), # CONSTRAINT pk_weather_weather PRIMARY KEY (location) sa.PrimaryKeyConstraint(
"location", name=op.f("pk_weather_weather")
), # CONSTRAINT pk_weather_weather PRIMARY KEY (location)
info={"bind_key": "weather"}, info={"bind_key": "weather"},
) )
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -245,7 +247,9 @@ from nonebot.typing import T_State
@weather.got("location", prompt="请输入地名") @weather.got("location", prompt="请输入地名")
async def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()): async def _(
state: T_State, session: async_scoped_session, location: str = ArgPlainText()
):
wea = await session.get(Weather, location) wea = await session.get(Weather, location)
if not wea: if not wea:
@@ -348,13 +352,16 @@ async def _(
```python title=weather/__init__.py {5} showLineNumbers ```python title=weather/__init__.py {5} showLineNumbers
from collections.abc import Sequence from collections.abc import Sequence
@weather.handle() @weather.handle()
async def _( async def _(
weas: Sequence[Weather] = SQLDepends( weas: Sequence[Weather] = SQLDepends(
select(Weather).where(Weather.weather == Depends(extract_arg_plain_text)) select(Weather).where(Weather.weather == Depends(extract_arg_plain_text))
), ),
): ):
await weather.send(f"今天的天气是{weas[0].weather}的城市有{''.join(wea.location for wea in weas)}") await weather.send(
f"今天的天气是{weas[0].weather}的城市有{''.join(wea.location for wea in weas)}"
)
``` ```
支持的类型标注请参见 [依赖注入](dependency)。 支持的类型标注请参见 [依赖注入](dependency)。
@@ -364,6 +371,7 @@ async def _(
```python title=weather/__init__.py {5-6,10} showLineNumbers ```python title=weather/__init__.py {5-6,10} showLineNumbers
from collections.abc import Sequence from collections.abc import Sequence
class Weather(Model): class Weather(Model):
location: Mapped[str] = mapped_column(primary_key=True) location: Mapped[str] = mapped_column(primary_key=True)
weather: Mapped[str] = Depends(extract_arg_plain_text) weather: Mapped[str] = Depends(extract_arg_plain_text)
@@ -78,8 +78,7 @@ async def html_to_pic(
img_fetch_fn: ImgFetchFn = combined_img_fetcher, img_fetch_fn: ImgFetchFn = combined_img_fetcher,
css_fetch_fn: CSSFetchFn = combined_css_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher,
urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin, urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin,
) -> bytes: ) -> bytes: ...
...
``` ```
最核心的渲染函数。 最核心的渲染函数。
@@ -107,8 +106,7 @@ async def text_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
可用于渲染多行文本。 可用于渲染多行文本。
@@ -128,8 +126,7 @@ async def md_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。 可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。
@@ -153,8 +150,7 @@ async def template_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
渲染 jinja2 模板。 渲染 jinja2 模板。
@@ -44,15 +44,18 @@ require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_apscheduler import scheduler
# 基于装饰器的方式 # 基于装饰器的方式
@scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2}) @scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2})
async def run_every_2_hour(arg1: int, arg2: int): async def run_every_2_hour(arg1: int, arg2: int):
pass pass
# 基于 add_job 方法的方式 # 基于 add_job 方法的方式
def run_every_day(arg1: int, arg2: int): def run_every_day(arg1: int, arg2: int):
pass pass
scheduler.add_job( scheduler.add_job(
run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2} run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2}
) )
@@ -25,6 +25,7 @@ NoneBot 中的网络通信主要包括以下几种:
```python {5,6} title=tests/test_http_server.py ```python {5,6} title=tests/test_http_server.py
from nonebug import App from nonebug import App
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_http_server(app: App): async def test_http_server(app: App):
async with app.test_server() as ctx: async with app.test_server() as ctx:
@@ -45,6 +46,7 @@ import nonebot
from nonebug import App from nonebug import App
from nonebot.adapters.fake import Adapter from nonebot.adapters.fake import Adapter
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_http_server(app: App): async def test_http_server(app: App):
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
@@ -72,6 +74,7 @@ import nonebot
from nonebug import App from nonebug import App
from nonebot.adapters.fake import Adapter from nonebot.adapters.fake import Adapter
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_ws_server(app: App): async def test_ws_server(app: App):
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
@@ -81,6 +81,7 @@ except Exception as e:
```python title=config.py ```python title=config.py
from pydantic import BaseModel from pydantic import BaseModel
class Config(BaseModel): class Config(BaseModel):
xxx_id: str xxx_id: str
xxx_token: str xxx_token: str
@@ -102,6 +103,7 @@ from nonebot.adapters import Adapter as BaseAdapter
from .config import Config from .config import Config
class Adapter(BaseAdapter): class Adapter(BaseAdapter):
@override @override
def __init__(self, driver: Driver, **kwargs: Any): def __init__(self, driver: Driver, **kwargs: Any):
@@ -208,9 +210,10 @@ from nonebot.drivers import (
ASGIMixin, ASGIMixin,
WebSocket, WebSocket,
HTTPServerSetup, HTTPServerSetup,
WebSocketServerSetup WebSocketServerSetup,
) )
class Adapter(BaseAdapter): class Adapter(BaseAdapter):
@override @override
def __init__(self, driver: Driver, **kwargs: Any): def __init__(self, driver: Driver, **kwargs: Any):
@@ -242,7 +245,6 @@ class Adapter(BaseAdapter):
) )
self.setup_websocket_server(ws_setup) self.setup_websocket_server(ws_setup)
async def _handle_http(self, request: Request) -> Response: async def _handle_http(self, request: Request) -> Response:
"""HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response""" """HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response"""
... ...
@@ -270,8 +272,8 @@ class Adapter(BaseAdapter):
```python {7,8,11} title=adapter.py ```python {7,8,11} title=adapter.py
from .bot import Bot from .bot import Bot
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
def _handle_connect(self): def _handle_connect(self):
bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID
bot = Bot(self, self_id=bot_id) # 实例化 Bot bot = Bot(self, self_id=bot_id) # 实例化 Bot
@@ -295,8 +297,8 @@ from .bot import Bot
from .event import Event from .event import Event
from .log import log from .log import log
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
@classmethod @classmethod
def payload_to_event(cls, payload: Dict[str, Any]) -> Event: def payload_to_event(cls, payload: Dict[str, Any]) -> Event:
"""根据平台事件的特性,转换平台 payload 为具体 Event """根据平台事件的特性,转换平台 payload 为具体 Event
@@ -316,7 +318,6 @@ class Adapter(BaseAdapter):
# 也可以尝试转为基础 Event 进行处理 # 也可以尝试转为基础 Event 进行处理
return type_validate_python(Event, payload) return type_validate_python(Event, payload)
async def _forward(self, bot: Bot): async def _forward(self, bot: Bot):
payload: Dict[str, Any] # 接收到的事件数据 payload: Dict[str, Any] # 接收到的事件数据
@@ -337,8 +338,8 @@ from nonebot.drivers import Request, WebSocket
from .bot import Bot from .bot import Bot
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
@override @override
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
log("DEBUG", f"Calling API <y>{api}</y>") # 给予日志提示 log("DEBUG", f"Calling API <y>{api}</y>") # 给予日志提示
@@ -356,7 +357,6 @@ class Adapter(BaseAdapter):
# 发送请求,返回结果 # 发送请求,返回结果
return await self.driver.request(request) return await self.driver.request(request)
# 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据 # 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据
# 通过某种方式获取到 bot 对应的 websocket 对象 # 通过某种方式获取到 bot 对应的 websocket 对象
ws: WebSocket = your_get_websocket_method(bot.self_id) ws: WebSocket = your_get_websocket_method(bot.self_id)
@@ -450,8 +450,8 @@ from typing_extensions import override
from nonebot.compat import model_dump from nonebot.compat import model_dump
from nonebot.adapters import Event as BaseEvent from nonebot.adapters import Event as BaseEvent
class Event(BaseEvent):
class Event(BaseEvent):
@override @override
def get_event_name(self) -> str: def get_event_name(self) -> str:
# 返回事件的名称,用于日志打印 # 返回事件的名称,用于日志打印
@@ -488,6 +488,7 @@ class Event(BaseEvent):
```python {7,16,20,25,34,42} title=event.py ```python {7,16,20,25,34,42} title=event.py
from .message import Message from .message import Message
class HeartbeatEvent(Event): class HeartbeatEvent(Event):
"""心跳时间,通常为元事件""" """心跳时间,通常为元事件"""
@@ -495,8 +496,10 @@ class HeartbeatEvent(Event):
def get_type(self) -> str: def get_type(self) -> str:
return "meta_event" return "meta_event"
class MessageEvent(Event): class MessageEvent(Event):
"""消息事件""" """消息事件"""
message_id: str message_id: str
user_id: str user_id: str
@@ -513,8 +516,10 @@ class MessageEvent(Event):
def get_user_id(self) -> str: def get_user_id(self) -> str:
return self.user_id return self.user_id
class JoinRoomEvent(Event): class JoinRoomEvent(Event):
"""加入房间事件,通常为通知事件""" """加入房间事件,通常为通知事件"""
user_id: str user_id: str
room_id: str room_id: str
@@ -522,8 +527,10 @@ class JoinRoomEvent(Event):
def get_type(self) -> str: def get_type(self) -> str:
return "notice" return "notice"
class ApplyAddFriendEvent(Event): class ApplyAddFriendEvent(Event):
"""申请添加好友事件,通常为请求事件""" """申请添加好友事件,通常为请求事件"""
user_id: str user_id: str
@override @override
@@ -544,6 +551,7 @@ from nonebot.utils import escape_tag
from nonebot.adapters import Message as BaseMessage from nonebot.adapters import Message as BaseMessage
from nonebot.adapters import MessageSegment as BaseMessageSegment from nonebot.adapters import MessageSegment as BaseMessageSegment
class MessageSegment(BaseMessageSegment["Message"]): class MessageSegment(BaseMessageSegment["Message"]):
@classmethod @classmethod
@override @override
@@ -591,6 +599,7 @@ class Message(BaseMessage[MessageSegment]):
```python title=tests/conftest.py ```python title=tests/conftest.py
from pathlib import Path from pathlib import Path
import nonebot.adapters import nonebot.adapters
nonebot.adapters.__path__.append( # type: ignore nonebot.adapters.__path__.append( # type: ignore
str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve()) str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve())
) )
@@ -48,7 +48,9 @@ weather = on_command("天气")
from nonebot import on_command from nonebot import on_command
from nonebot.rule import to_me from nonebot.rule import to_me
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) weather = on_command(
"天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True
)
``` ```
这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。 这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。
@@ -174,9 +174,7 @@ message = Message(
# 索引 # 索引
message[0] == MessageSegment.text("test") message[0] == MessageSegment.text("test")
# 切片 # 切片
message[0:2] == Message( message[0:2] == Message([MessageSegment.text("test"), MessageSegment.markdown("test2")])
[MessageSegment.text("test"), MessageSegment.markdown("test2")]
)
# 类型过滤 # 类型过滤
message["markdown"] == Message( message["markdown"] == Message(
[MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")]
@@ -262,7 +260,7 @@ msg = seg.join(
MessageSegment.text("second"), MessageSegment.text("second"),
MessageSegment.text("third"), MessageSegment.text("third"),
] ]
) ),
] ]
) )
msg == Message( msg == Message(
@@ -23,8 +23,8 @@ NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但
```python ```python
from nonebot.matcher import MatcherProvider from nonebot.matcher import MatcherProvider
class CustomProvider(MatcherProvider):
... class CustomProvider(MatcherProvider): ...
``` ```
## 设置存储提供者 ## 设置存储提供者
@@ -48,9 +48,11 @@ NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServer
from nonebot import get_driver from nonebot import get_driver
from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup
async def hello(request: Request) -> Response: async def hello(request: Request) -> Response:
return Response(200, content="Hello, world!") return Response(200, content="Hello, world!")
if isinstance((driver := get_driver()), ASGIMixin): if isinstance((driver := get_driver()), ASGIMixin):
driver.setup_http_server( driver.setup_http_server(
HTTPServerSetup( HTTPServerSetup(
@@ -78,6 +80,7 @@ if isinstance((driver := get_driver()), ASGIMixin):
from nonebot import get_driver from nonebot import get_driver
from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup
async def ws_handler(ws: WebSocket): async def ws_handler(ws: WebSocket):
await ws.accept() await ws.accept()
try: try:
@@ -92,6 +95,7 @@ async def ws_handler(ws: WebSocket):
await websocket.close() await websocket.close()
# do some cleanup # do some cleanup
if isinstance((driver := get_driver()), ASGIMixin): if isinstance((driver := get_driver()), ASGIMixin):
driver.setup_websocket_server( driver.setup_websocket_server(
WebSocketServerSetup( WebSocketServerSetup(
@@ -129,6 +133,7 @@ from fastapi import FastAPI
app: FastAPI = nonebot.get_app() app: FastAPI = nonebot.get_app()
@app.get("/api") @app.get("/api")
async def custom_api(): async def custom_api():
return {"message": "Hello, world!"} return {"message": "Hello, world!"}
@@ -29,6 +29,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_startup @driver.on_startup
async def do_something(): async def do_something():
pass pass
@@ -43,6 +44,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_shutdown @driver.on_shutdown
async def do_something(): async def do_something():
pass pass
@@ -57,6 +59,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_bot_connect @driver.on_bot_connect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -71,6 +74,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_bot_disconnect @driver.on_bot_disconnect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -88,6 +92,7 @@ async def do_something(bot: Bot):
from nonebot.exception import IgnoredException from nonebot.exception import IgnoredException
from nonebot.message import event_preprocessor from nonebot.message import event_preprocessor
@event_preprocessor @event_preprocessor
async def do_something(event: Event): async def do_something(event: Event):
if not event.is_tome(): if not event.is_tome():
@@ -101,6 +106,7 @@ async def do_something(event: Event):
```python ```python
from nonebot.message import event_postprocessor from nonebot.message import event_postprocessor
@event_postprocessor @event_postprocessor
async def do_something(event: Event): async def do_something(event: Event):
pass pass
@@ -114,6 +120,7 @@ async def do_something(event: Event):
from nonebot.message import run_preprocessor from nonebot.message import run_preprocessor
from nonebot.exception import IgnoredException from nonebot.exception import IgnoredException
@run_preprocessor @run_preprocessor
async def do_something(event: Event, matcher: Matcher): async def do_something(event: Event, matcher: Matcher):
if not event.is_tome(): if not event.is_tome():
@@ -127,6 +134,7 @@ async def do_something(event: Event, matcher: Matcher):
```python ```python
from nonebot.message import run_postprocessor from nonebot.message import run_postprocessor
@run_postprocessor @run_postprocessor
async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]): async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]):
pass pass
@@ -140,6 +148,7 @@ async def do_something(event: Event, matcher: Matcher, exception: Optional[Excep
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
@Bot.on_calling_api @Bot.on_calling_api
async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):
if api == "send_msg": if api == "send_msg":
@@ -154,9 +163,14 @@ async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
@Bot.on_called_api @Bot.on_called_api
async def handle_api_result( async def handle_api_result(
bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any bot: Bot,
exception: Optional[Exception],
api: str,
data: Dict[str, Any],
result: Any,
): ):
if not exception and api == "send_msg": if not exception and api == "send_msg":
raise MockApiException(result={**result, "message_id": 123}) raise MockApiException(result={**result, "message_id": 123})
@@ -21,6 +21,7 @@ options:
```python {3-5} ```python {3-5}
foo = on_message() foo = on_message()
@foo.type_updater @foo.type_updater
async def _() -> str: async def _() -> str:
return "notice" return "notice"
@@ -37,6 +38,7 @@ from nonebot.permission import User
foo = on_message() foo = on_message()
@foo.permission_updater @foo.permission_updater
async def _(event: Event, matcher: Matcher) -> Permission: async def _(event: Event, matcher: Matcher) -> Permission:
return Permission(User.from_event(event, perm=matcher.permission)) return Permission(User.from_event(event, perm=matcher.permission))
@@ -49,6 +51,7 @@ from nonebot.permission import USER
foo = on_message() foo = on_message()
@foo.permission_updater @foo.permission_updater
async def _(matcher: Matcher) -> Permission: async def _(matcher: Matcher) -> Permission:
return USER("session1", "session2", perm=matcher.permission) return USER("session1", "session2", perm=matcher.permission)
@@ -119,6 +119,8 @@ NoneBotException
```python ```python
matcher = on_notice(block=True) matcher = on_notice(block=True)
# 或者 # 或者
@matcher.handle() @matcher.handle()
async def handler(matcher: Matcher): async def handler(matcher: Matcher):
@@ -100,6 +100,7 @@ description: nonebot 模块
```python ```python
from nonebot.adapters.console import Adapter from nonebot.adapters.console import Adapter
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
``` ```
@@ -142,17 +142,18 @@ description: nonebot.params 模块
def depend_func() -> Any: def depend_func() -> Any:
return ... return ...
def depend_gen_func(): def depend_gen_func():
try: try:
yield ... yield ...
finally: finally:
... ...
async def handler( async def handler(
param_name: Any = Depends(depend_func), param_name: Any = Depends(depend_func),
gen: Any = Depends(depend_gen_func), gen: Any = Depends(depend_gen_func),
): ): ...
...
``` ```
## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam} ## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam}
@@ -76,7 +76,7 @@ logger.add(
level=0, level=0,
diagnose=True, diagnose=True,
format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}", format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}",
filter=default_filter filter=default_filter,
) )
``` ```
@@ -21,6 +21,7 @@ options:
```python {4} title=weather/__init__.py ```python {4} title=weather/__init__.py
from nonebot.adapters.console import MessageEvent from nonebot.adapters.console import MessageEvent
@weather.got("location", prompt="请输入地名") @weather.got("location", prompt="请输入地名")
async def got_location(event: MessageEvent, location: str = ArgPlainText()): async def got_location(event: MessageEvent, location: str = ArgPlainText()):
await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...") await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...")
@@ -39,10 +40,12 @@ async def got_location(event: MessageEvent, location: str = ArgPlainText()):
```python {4,8} ```python {4,8}
from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent
@matcher.handle() @matcher.handle()
async def handle_private(event: PrivateMessageEvent): async def handle_private(event: PrivateMessageEvent):
await matcher.finish("私聊消息") await matcher.finish("私聊消息")
@matcher.handle() @matcher.handle()
async def handle_group(event: GroupMessageEvent): async def handle_group(event: GroupMessageEvent):
await matcher.finish("群聊消息") await matcher.finish("群聊消息")
@@ -54,10 +57,12 @@ async def handle_group(event: GroupMessageEvent):
from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.console import Bot as ConsoleBot
from nonebot.adapters.onebot.v11 import Bot as OneBot from nonebot.adapters.onebot.v11 import Bot as OneBot
@matcher.handle() @matcher.handle()
async def handle_console(bot: ConsoleBot): async def handle_console(bot: ConsoleBot):
await bot.bell() await bot.bell()
@matcher.handle() @matcher.handle()
async def handle_onebot(bot: OneBot): async def handle_onebot(bot: OneBot):
await bot.send_group_message(group_id=123123, message="OneBot") await bot.send_group_message(group_id=123123, message="OneBot")
@@ -27,9 +27,11 @@ from .config import Config
plugin_config = get_plugin_config(Config) plugin_config = get_plugin_config(Config)
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
weather = on_command("天气", rule=is_enable) weather = on_command("天气", rule=is_enable)
``` ```
@@ -43,12 +45,15 @@ weather = on_command("天气", rule=is_enable)
from nonebot.rule import Rule from nonebot.rule import Rule
from nonebot.adapters import Event from nonebot.adapters import Event
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
async def is_blacklisted(event: Event) -> bool: async def is_blacklisted(event: Event) -> bool:
return event.get_user_id() not in BLACKLIST return event.get_user_id() not in BLACKLIST
rule = Rule(is_enable, is_blacklisted) rule = Rule(is_enable, is_blacklisted)
weather = on_command("天气", rule=rule) weather = on_command("天气", rule=rule)
@@ -66,9 +71,11 @@ from .config import Config
plugin_config = get_plugin_config(Config) plugin_config = get_plugin_config(Config)
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
weather = on_command( weather = on_command(
"天气", "天气",
rule=to_me() & is_enable, rule=to_me() & is_enable,
@@ -17,6 +17,7 @@ NoneBot 中的会话状态是一个字典,可以通过类型 `T_State` 来获
```python ```python
from nonebot.typing import T_State from nonebot.typing import T_State
@matcher.got("key", prompt="请输入密码") @matcher.got("key", prompt="请输入密码")
async def _(state: T_State, key: str = ArgPlainText()): async def _(state: T_State, key: str = ArgPlainText()):
if key != "some password": if key != "some password":
@@ -34,10 +35,12 @@ async def _(state: T_State, key: str = ArgPlainText()):
```python ```python
from nonebot.typing import T_State from nonebot.typing import T_State
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
state["key"] = "value" state["key"] = "value"
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
await matcher.finish(state["key"]) await matcher.finish(state["key"])
@@ -49,10 +52,12 @@ async def _(state: T_State):
from nonebot.typing import T_State from nonebot.typing import T_State
from nonebot.adapters import MessageTemplate from nonebot.adapters import MessageTemplate
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
state["username"] = "user" state["username"] = "user"
@matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码")) @matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码"))
async def _(): async def _():
await matcher.finish(MessageTemplate("密码为 {password}")) await matcher.finish(MessageTemplate("密码为 {password}"))
@@ -20,7 +20,7 @@ alc = Alconna(
Args["package", str], Args["package", str],
Option("-r|--requirement", Args["file", str]), Option("-r|--requirement", Args["file", str]),
Option("-i|--index-url", Args["url", str]), Option("-i|--index-url", Args["url", str]),
) ),
) )
res = alc.parse("pip install nonebot2 -i URL") res = alc.parse("pip install nonebot2 -i URL")
@@ -383,20 +383,33 @@ alc = Alconna(..., meta=CommandMeta("foo", example="bar"))
from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config
ns = Namespace("foo", prefixes=["/"]) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ ns = Namespace(
"foo", prefixes=["/"]
) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/
alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 alc = Alconna(
"pip", Subcommand("install", Args["package", str]), namespace=ns
) # 在创建Alconna时候传入命名空间以替换默认命名空间
# 可以通过with方式创建命名空间 # 可以通过with方式创建命名空间
with namespace("bar") as np1: with namespace("bar") as np1:
np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 np1.prefixes = [
"!"
] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令
np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter
np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 np1.builtin_option_name["help"] = {
"帮助",
"-h",
} # 设置此命名空间下的命令的帮助选项名称
# 你还可以使用config来管理所有命名空间并切换至任意命名空间 # 你还可以使用config来管理所有命名空间并切换至任意命名空间
config.namespaces["foo"] = ns # 将命名空间挂载到 config 上 config.namespaces["foo"] = ns # 将命名空间挂载到 config 上
alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间 alc = Alconna(
"pip",
Subcommand("install", Args["package", str]),
namespace=config.namespaces["foo"],
) # 也是同样可以切换到"foo"命名空间
``` ```
### 修改默认的命名空间 ### 修改默认的命名空间
@@ -469,10 +482,12 @@ alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"})
alc.shortcut("echo", delete=True) # 删除快捷指令 alc.shortcut("echo", delete=True) # 删除快捷指令
# 'Alconna::eval 的快捷指令: "echo" 删除成功' # 'Alconna::eval 的快捷指令: "echo" 删除成功'
@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器 @alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器
def cb(content: str): def cb(content: str):
eval(content, {}, {}) eval(content, {}, {})
alc.parse('eval print(\\"hello world\\")') alc.parse('eval print(\\"hello world\\")')
# hello world # hello world
@@ -523,7 +538,12 @@ alc.parse("eval --shortcut list")
from arclet.alconna import Alconna, Option, CommandMeta, Args from arclet.alconna import Alconna, Option, CommandMeta, Args
alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) alc = Alconna(
"test",
Args["foo", int],
Option("BAR", Args["baz", str], compact=True),
meta=CommandMeta(compact=True),
)
assert alc.parse("test123 BARabc").matched assert alc.parse("test123 BARabc").matched
``` ```
@@ -534,7 +554,9 @@ assert alc.parse("test123 BARabc").matched
from arclet.alconna import Alconna, Option, Args, append from arclet.alconna import Alconna, Option, Args, append
alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) alc = Alconna(
"gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)
)
print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content"))
# ['abc', 'def', 'xyz'] # ['abc', 'def', 'xyz']
``` ```
@@ -577,7 +599,7 @@ from arclet.alconna import Alconna, Args, Option
alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar")
alc.parse("test --comp") alc.parse("test --comp")
''' """
output output
以下是建议的输入: 以下是建议的输入:
@@ -588,7 +610,7 @@ output
* --shortcut * --shortcut
* foo * foo
* bar * bar
''' """
``` ```
## Duplication ## Duplication
@@ -600,7 +622,16 @@ output
以pip为例,其对应的 Duplication 应如下构造: 以pip为例,其对应的 Duplication 应如下构造:
```python ```python
from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count from arclet.alconna import (
Alconna,
Args,
Option,
OptionResult,
Duplication,
SubcommandStub,
Subcommand,
count,
)
class MyDup(Duplication): class MyDup(Duplication):
@@ -29,10 +29,10 @@ from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
echo = on_alconna(Alconna("echo", Args["msg", str])) echo = on_alconna(Alconna("echo", Args["msg", str]))
@echo.handle() @echo.handle()
async def echo_exit(msg: Match[str] = AlconnaMatch("msg")): async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
await echo.finish(msg.result) await echo.finish(msg.result)
``` ```
相比于 `on_alconna` `funcommand` 增加了三个参数 `name`, `prefixes``description` 相比于 `on_alconna` `funcommand` 增加了三个参数 `name`, `prefixes``description`
@@ -54,6 +54,7 @@ book = (
.build() .build()
) )
@book.handle() @book.handle()
async def _(arp: Arparma): async def _(arp: Arparma):
await book.send(str(arp.options)) await book.send(str(arp.options))
@@ -45,14 +45,11 @@ message = UniMessage(
```python ```python
from nonebot_plugin_alconna import Button, UniMessage from nonebot_plugin_alconna import Button, UniMessage
message = ( message = UniMessage.text("hello world").keyboard(
UniMessage.text("hello world")
.keyboard(
Button("link1", url="https://example.com/1"), Button("link1", url="https://example.com/1"),
Button("link2", url="https://example.com/2"), Button("link2", url="https://example.com/2"),
Button("link3", url="https://example.com/3"), Button("link3", url="https://example.com/3"),
row=3, row=3,
)
) )
``` ```
@@ -94,6 +91,7 @@ async def _():
```python ```python
from nonebot_plugin_alconna import message_recall, message_edit, message_reaction from nonebot_plugin_alconna import message_recall, message_edit, message_reaction
@matcher.handle() @matcher.handle()
async def _(): async def _():
await message_edit(UniMessage.text("hello world")) await message_edit(UniMessage.text("hello world"))
@@ -120,9 +118,9 @@ async def _():
```python ```python
from nonebot_plugin_alconna import UniMsg from nonebot_plugin_alconna import UniMsg
@matcher.handle() @matcher.handle()
async def _(msg: UniMsg): async def _(msg: UniMsg): ...
...
``` ```
然后你可以通过 `UniMessage` 的方法来处理消息. 然后你可以通过 `UniMessage` 的方法来处理消息.
@@ -182,6 +180,7 @@ async def _(msg: UniMsg):
```python ```python
from nonebot_plugin_alconna import UniMsg from nonebot_plugin_alconna import UniMsg
@matcher.handle() @matcher.handle()
async def _(msg: UniMsg): async def _(msg: UniMsg):
data: list[dict] = msg.dump() data: list[dict] = msg.dump()
@@ -193,6 +192,7 @@ async def _(msg: UniMsg):
```python ```python
from nonebot_plugin_alconna import UniMessage from nonebot_plugin_alconna import UniMessage
@matcher.handle() @matcher.handle()
async def _(): async def _():
data = [ data = [
@@ -12,9 +12,9 @@ from nonebot_plugin_alconna import Alconna, Args, Image, on_alconna
meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image])) meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image]))
@meme.handle() @meme.handle()
async def _(img: Image): async def _(img: Image): ...
...
``` ```
## 模型定义 ## 模型定义
@@ -24,6 +24,7 @@ async def _(img: Image):
```python ```python
class Segment: class Segment:
"""基类标注""" """基类标注"""
@property @property
def type(self) -> str: ... def type(self) -> str: ...
@property @property
@@ -31,29 +32,40 @@ class Segment:
@property @property
def children(self) -> list["Segment"]: ... def children(self) -> list["Segment"]: ...
class Text(Segment): class Text(Segment):
"""Text对象, 表示一类文本元素""" """Text对象, 表示一类文本元素"""
text: str text: str
styles: dict[tuple[int, int], list[str]] styles: dict[tuple[int, int], list[str]]
def cover(self, text: str): ... def cover(self, text: str): ...
def mark(self, start: Optional[int] = None, end: Optional[int] = None, *styles: str): ... def mark(
self, start: Optional[int] = None, end: Optional[int] = None, *styles: str
): ...
class At(Segment): class At(Segment):
"""At对象, 表示一类提醒某用户的元素""" """At对象, 表示一类提醒某用户的元素"""
flag: Literal["user", "role", "channel"] flag: Literal["user", "role", "channel"]
target: str target: str
display: Optional[str] display: Optional[str]
class AtAll(Segment): class AtAll(Segment):
"""AtAll对象, 表示一类提醒所有人的元素""" """AtAll对象, 表示一类提醒所有人的元素"""
here: bool here: bool
class Emoji(Segment): class Emoji(Segment):
"""Emoji对象, 表示一类表情元素""" """Emoji对象, 表示一类表情元素"""
id: str id: str
name: Optional[str] name: Optional[str]
class Media(Segment): class Media(Segment):
id: Optional[str] id: Optional[str]
url: Optional[str] url: Optional[str]
@@ -64,53 +76,72 @@ class Media(Segment):
to_url: ClassVar[Optional[MediaToUrl]] to_url: ClassVar[Optional[MediaToUrl]]
class Image(Media): class Image(Media):
"""Image对象, 表示一类图片元素""" """Image对象, 表示一类图片元素"""
width: Optional[int] width: Optional[int]
height: Optional[int] height: Optional[int]
class Audio(Media): class Audio(Media):
"""Audio对象, 表示一类音频元素""" """Audio对象, 表示一类音频元素"""
duration: Optional[float] duration: Optional[float]
class Voice(Media): class Voice(Media):
"""Voice对象, 表示一类语音元素""" """Voice对象, 表示一类语音元素"""
duration: Optional[float] duration: Optional[float]
class Video(Media): class Video(Media):
"""Video对象, 表示一类视频元素""" """Video对象, 表示一类视频元素"""
thumbnail: Optional[Image] thumbnail: Optional[Image]
duration: Optional[float] duration: Optional[float]
class File(Media): class File(Media):
"""File对象, 表示一类文件元素""" """File对象, 表示一类文件元素"""
class Reply(Segment): class Reply(Segment):
"""Reply对象,表示一类回复消息""" """Reply对象,表示一类回复消息"""
id: str id: str
"""此处不一定是消息ID,可能是其他ID,如消息序号等""" """此处不一定是消息ID,可能是其他ID,如消息序号等"""
msg: Optional[Union[Message, str]] msg: Optional[Union[Message, str]]
origin: Optional[Any] origin: Optional[Any]
class Reference(Segment): class Reference(Segment):
"""Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类""" """Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类"""
id: Optional[str] id: Optional[str]
"""此处不一定是消息ID,可能是其他ID,如消息序号等""" """此处不一定是消息ID,可能是其他ID,如消息序号等"""
children: List[Union[RefNode, CustomNode]] children: List[Union[RefNode, CustomNode]]
class Hyper(Segment): class Hyper(Segment):
"""Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等""" """Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等"""
format: Literal["xml", "json"] format: Literal["xml", "json"]
raw: Optional[str] raw: Optional[str]
content: Optional[Union[dict, list]] content: Optional[Union[dict, list]]
class Reference(Segment): class Reference(Segment):
"""Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类""" """Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类"""
id: Optional[str] id: Optional[str]
nodes: Sequence[Union[RefNode, CustomNode]] nodes: Sequence[Union[RefNode, CustomNode]]
class Button(Segment): class Button(Segment):
"""Button对象,表示一类按钮消息""" """Button对象,表示一类按钮消息"""
flag: Literal["action", "link", "input", "enter"] flag: Literal["action", "link", "input", "enter"]
""" """
- 点击 action 类型的按钮时会触发一个关于 按钮回调 事件,该事件的 button 资源会包含上述 id - 点击 action 类型的按钮时会触发一个关于 按钮回调 事件,该事件的 button 资源会包含上述 id
@@ -138,20 +169,26 @@ class Button(Segment):
- list[At]: 指定用户/身份组可操作 - list[At]: 指定用户/身份组可操作
""" """
class Keyboard(Segment): class Keyboard(Segment):
"""Keyboard对象,表示一行按钮元素""" """Keyboard对象,表示一行按钮元素"""
id: Optional[str] id: Optional[str]
"""此处一般用来表示模板id,特殊情况下可能表示例如 bot_appid 等""" """此处一般用来表示模板id,特殊情况下可能表示例如 bot_appid 等"""
buttons: Optional[list[Button]] buttons: Optional[list[Button]]
row: Optional[int] row: Optional[int]
"""当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数""" """当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数"""
class Other(Segment): class Other(Segment):
"""其他 Segment""" """其他 Segment"""
origin: MessageSegment origin: MessageSegment
class I18n(Segment): class I18n(Segment):
"""特殊的 Segment,用于 i18n 消息""" """特殊的 Segment,用于 i18n 消息"""
item_or_scope: Union[LangItem, str] item_or_scope: Union[LangItem, str]
type_: Optional[str] = None type_: Optional[str] = None
@@ -172,10 +209,14 @@ from nonebot_plugin_alconna import Args, Image, Alconna, select
from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace
# 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果 # 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果
alc1 = Alconna("make_meme", Args["name", str]["img", select(Image).first]) # 也可以使用 select(Image).nth(0) alc1 = Alconna(
"make_meme", Args["name", str]["img", select(Image).first]
) # 也可以使用 select(Image).nth(0)
# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image # 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image
alc2 = Alconna("make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]]) alc2 = Alconna(
"make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]]
)
``` ```
也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取) 也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取)
@@ -213,10 +254,13 @@ def mfbuild(builder: MessageBuilder, seg: BaseMessageSegment):
@custom_handler(MarketFace) @custom_handler(MarketFace)
async def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool): async def mfexport(
exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool
):
if exporter.get_message_type() is Message: if exporter.get_message_type() is Message:
return MessageSegment("chronocat:marketface", seg.data)(await exporter.export(seg.children, bot, fallback)) return MessageSegment("chronocat:marketface", seg.data)(
await exporter.export(seg.children, bot, fallback)
)
``` ```
具体而言,你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。 具体而言,你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。
@@ -155,7 +155,9 @@ op.create_table( # CREATE TABLE
"weather_weather", # weather_weather "weather_weather", # weather_weather
sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL, sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL,
sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL, sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL,
sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), # CONSTRAINT pk_weather_weather PRIMARY KEY (location) sa.PrimaryKeyConstraint(
"location", name=op.f("pk_weather_weather")
), # CONSTRAINT pk_weather_weather PRIMARY KEY (location)
info={"bind_key": "weather"}, info={"bind_key": "weather"},
) )
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -245,7 +247,9 @@ from nonebot.typing import T_State
@weather.got("location", prompt="请输入地名") @weather.got("location", prompt="请输入地名")
async def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()): async def _(
state: T_State, session: async_scoped_session, location: str = ArgPlainText()
):
wea = await session.get(Weather, location) wea = await session.get(Weather, location)
if not wea: if not wea:
@@ -348,13 +352,16 @@ async def _(
```python title=weather/__init__.py {5} showLineNumbers ```python title=weather/__init__.py {5} showLineNumbers
from collections.abc import Sequence from collections.abc import Sequence
@weather.handle() @weather.handle()
async def _( async def _(
weas: Sequence[Weather] = SQLDepends( weas: Sequence[Weather] = SQLDepends(
select(Weather).where(Weather.weather == Depends(extract_arg_plain_text)) select(Weather).where(Weather.weather == Depends(extract_arg_plain_text))
), ),
): ):
await weather.send(f"今天的天气是{weas[0].weather}的城市有{''.join(wea.location for wea in weas)}") await weather.send(
f"今天的天气是{weas[0].weather}的城市有{''.join(wea.location for wea in weas)}"
)
``` ```
支持的类型标注请参见 [依赖注入](dependency)。 支持的类型标注请参见 [依赖注入](dependency)。
@@ -364,6 +371,7 @@ async def _(
```python title=weather/__init__.py {5-6,10} showLineNumbers ```python title=weather/__init__.py {5-6,10} showLineNumbers
from collections.abc import Sequence from collections.abc import Sequence
class Weather(Model): class Weather(Model):
location: Mapped[str] = mapped_column(primary_key=True) location: Mapped[str] = mapped_column(primary_key=True)
weather: Mapped[str] = Depends(extract_arg_plain_text) weather: Mapped[str] = Depends(extract_arg_plain_text)
@@ -78,8 +78,7 @@ async def html_to_pic(
img_fetch_fn: ImgFetchFn = combined_img_fetcher, img_fetch_fn: ImgFetchFn = combined_img_fetcher,
css_fetch_fn: CSSFetchFn = combined_css_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher,
urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin, urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin,
) -> bytes: ) -> bytes: ...
...
``` ```
最核心的渲染函数。 最核心的渲染函数。
@@ -107,8 +106,7 @@ async def text_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
可用于渲染多行文本。 可用于渲染多行文本。
@@ -128,8 +126,7 @@ async def md_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。 可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。
@@ -153,8 +150,7 @@ async def template_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
渲染 jinja2 模板。 渲染 jinja2 模板。
@@ -44,15 +44,18 @@ require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_apscheduler import scheduler
# 基于装饰器的方式 # 基于装饰器的方式
@scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2}) @scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2})
async def run_every_2_hour(arg1: int, arg2: int): async def run_every_2_hour(arg1: int, arg2: int):
pass pass
# 基于 add_job 方法的方式 # 基于 add_job 方法的方式
def run_every_day(arg1: int, arg2: int): def run_every_day(arg1: int, arg2: int):
pass pass
scheduler.add_job( scheduler.add_job(
run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2} run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2}
) )
@@ -25,6 +25,7 @@ NoneBot 中的网络通信主要包括以下几种:
```python {5,6} title=tests/test_http_server.py ```python {5,6} title=tests/test_http_server.py
from nonebug import App from nonebug import App
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_http_server(app: App): async def test_http_server(app: App):
async with app.test_server() as ctx: async with app.test_server() as ctx:
@@ -45,6 +46,7 @@ import nonebot
from nonebug import App from nonebug import App
from nonebot.adapters.fake import Adapter from nonebot.adapters.fake import Adapter
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_http_server(app: App): async def test_http_server(app: App):
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
@@ -72,6 +74,7 @@ import nonebot
from nonebug import App from nonebug import App
from nonebot.adapters.fake import Adapter from nonebot.adapters.fake import Adapter
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_ws_server(app: App): async def test_ws_server(app: App):
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
@@ -81,6 +81,7 @@ except Exception as e:
```python title=config.py ```python title=config.py
from pydantic import BaseModel from pydantic import BaseModel
class Config(BaseModel): class Config(BaseModel):
xxx_id: str xxx_id: str
xxx_token: str xxx_token: str
@@ -102,6 +103,7 @@ from nonebot.adapters import Adapter as BaseAdapter
from .config import Config from .config import Config
class Adapter(BaseAdapter): class Adapter(BaseAdapter):
@override @override
def __init__(self, driver: Driver, **kwargs: Any): def __init__(self, driver: Driver, **kwargs: Any):
@@ -208,9 +210,10 @@ from nonebot.drivers import (
ASGIMixin, ASGIMixin,
WebSocket, WebSocket,
HTTPServerSetup, HTTPServerSetup,
WebSocketServerSetup WebSocketServerSetup,
) )
class Adapter(BaseAdapter): class Adapter(BaseAdapter):
@override @override
def __init__(self, driver: Driver, **kwargs: Any): def __init__(self, driver: Driver, **kwargs: Any):
@@ -242,7 +245,6 @@ class Adapter(BaseAdapter):
) )
self.setup_websocket_server(ws_setup) self.setup_websocket_server(ws_setup)
async def _handle_http(self, request: Request) -> Response: async def _handle_http(self, request: Request) -> Response:
"""HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response""" """HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response"""
... ...
@@ -270,8 +272,8 @@ class Adapter(BaseAdapter):
```python {7,8,11} title=adapter.py ```python {7,8,11} title=adapter.py
from .bot import Bot from .bot import Bot
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
def _handle_connect(self): def _handle_connect(self):
bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID
bot = Bot(self, self_id=bot_id) # 实例化 Bot bot = Bot(self, self_id=bot_id) # 实例化 Bot
@@ -295,8 +297,8 @@ from .bot import Bot
from .event import Event from .event import Event
from .log import log from .log import log
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
@classmethod @classmethod
def payload_to_event(cls, payload: Dict[str, Any]) -> Event: def payload_to_event(cls, payload: Dict[str, Any]) -> Event:
"""根据平台事件的特性,转换平台 payload 为具体 Event """根据平台事件的特性,转换平台 payload 为具体 Event
@@ -316,7 +318,6 @@ class Adapter(BaseAdapter):
# 也可以尝试转为基础 Event 进行处理 # 也可以尝试转为基础 Event 进行处理
return type_validate_python(Event, payload) return type_validate_python(Event, payload)
async def _forward(self, bot: Bot): async def _forward(self, bot: Bot):
payload: Dict[str, Any] # 接收到的事件数据 payload: Dict[str, Any] # 接收到的事件数据
@@ -337,8 +338,8 @@ from nonebot.drivers import Request, WebSocket
from .bot import Bot from .bot import Bot
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
@override @override
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
log("DEBUG", f"Calling API <y>{api}</y>") # 给予日志提示 log("DEBUG", f"Calling API <y>{api}</y>") # 给予日志提示
@@ -356,7 +357,6 @@ class Adapter(BaseAdapter):
# 发送请求,返回结果 # 发送请求,返回结果
return await self.driver.request(request) return await self.driver.request(request)
# 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据 # 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据
# 通过某种方式获取到 bot 对应的 websocket 对象 # 通过某种方式获取到 bot 对应的 websocket 对象
ws: WebSocket = your_get_websocket_method(bot.self_id) ws: WebSocket = your_get_websocket_method(bot.self_id)
@@ -450,8 +450,8 @@ from typing_extensions import override
from nonebot.compat import model_dump from nonebot.compat import model_dump
from nonebot.adapters import Event as BaseEvent from nonebot.adapters import Event as BaseEvent
class Event(BaseEvent):
class Event(BaseEvent):
@override @override
def get_event_name(self) -> str: def get_event_name(self) -> str:
# 返回事件的名称,用于日志打印 # 返回事件的名称,用于日志打印
@@ -488,6 +488,7 @@ class Event(BaseEvent):
```python {7,16,20,25,34,42} title=event.py ```python {7,16,20,25,34,42} title=event.py
from .message import Message from .message import Message
class HeartbeatEvent(Event): class HeartbeatEvent(Event):
"""心跳时间,通常为元事件""" """心跳时间,通常为元事件"""
@@ -495,8 +496,10 @@ class HeartbeatEvent(Event):
def get_type(self) -> str: def get_type(self) -> str:
return "meta_event" return "meta_event"
class MessageEvent(Event): class MessageEvent(Event):
"""消息事件""" """消息事件"""
message_id: str message_id: str
user_id: str user_id: str
@@ -513,8 +516,10 @@ class MessageEvent(Event):
def get_user_id(self) -> str: def get_user_id(self) -> str:
return self.user_id return self.user_id
class JoinRoomEvent(Event): class JoinRoomEvent(Event):
"""加入房间事件,通常为通知事件""" """加入房间事件,通常为通知事件"""
user_id: str user_id: str
room_id: str room_id: str
@@ -522,8 +527,10 @@ class JoinRoomEvent(Event):
def get_type(self) -> str: def get_type(self) -> str:
return "notice" return "notice"
class ApplyAddFriendEvent(Event): class ApplyAddFriendEvent(Event):
"""申请添加好友事件,通常为请求事件""" """申请添加好友事件,通常为请求事件"""
user_id: str user_id: str
@override @override
@@ -544,6 +551,7 @@ from nonebot.utils import escape_tag
from nonebot.adapters import Message as BaseMessage from nonebot.adapters import Message as BaseMessage
from nonebot.adapters import MessageSegment as BaseMessageSegment from nonebot.adapters import MessageSegment as BaseMessageSegment
class MessageSegment(BaseMessageSegment["Message"]): class MessageSegment(BaseMessageSegment["Message"]):
@classmethod @classmethod
@override @override
@@ -591,6 +599,7 @@ class Message(BaseMessage[MessageSegment]):
```python title=tests/conftest.py ```python title=tests/conftest.py
from pathlib import Path from pathlib import Path
import nonebot.adapters import nonebot.adapters
nonebot.adapters.__path__.append( # type: ignore nonebot.adapters.__path__.append( # type: ignore
str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve()) str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve())
) )
@@ -48,7 +48,9 @@ weather = on_command("天气")
from nonebot import on_command from nonebot import on_command
from nonebot.rule import to_me from nonebot.rule import to_me
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) weather = on_command(
"天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True
)
``` ```
这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。 这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。
@@ -174,9 +174,7 @@ message = Message(
# 索引 # 索引
message[0] == MessageSegment.text("test") message[0] == MessageSegment.text("test")
# 切片 # 切片
message[0:2] == Message( message[0:2] == Message([MessageSegment.text("test"), MessageSegment.markdown("test2")])
[MessageSegment.text("test"), MessageSegment.markdown("test2")]
)
# 类型过滤 # 类型过滤
message["markdown"] == Message( message["markdown"] == Message(
[MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")]
@@ -262,7 +260,7 @@ msg = seg.join(
MessageSegment.text("second"), MessageSegment.text("second"),
MessageSegment.text("third"), MessageSegment.text("third"),
] ]
) ),
] ]
) )
msg == Message( msg == Message(
@@ -23,8 +23,8 @@ NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但
```python ```python
from nonebot.matcher import MatcherProvider from nonebot.matcher import MatcherProvider
class CustomProvider(MatcherProvider):
... class CustomProvider(MatcherProvider): ...
``` ```
## 设置存储提供者 ## 设置存储提供者
@@ -48,9 +48,11 @@ NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServer
from nonebot import get_driver from nonebot import get_driver
from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup
async def hello(request: Request) -> Response: async def hello(request: Request) -> Response:
return Response(200, content="Hello, world!") return Response(200, content="Hello, world!")
if isinstance((driver := get_driver()), ASGIMixin): if isinstance((driver := get_driver()), ASGIMixin):
driver.setup_http_server( driver.setup_http_server(
HTTPServerSetup( HTTPServerSetup(
@@ -78,6 +80,7 @@ if isinstance((driver := get_driver()), ASGIMixin):
from nonebot import get_driver from nonebot import get_driver
from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup
async def ws_handler(ws: WebSocket): async def ws_handler(ws: WebSocket):
await ws.accept() await ws.accept()
try: try:
@@ -92,6 +95,7 @@ async def ws_handler(ws: WebSocket):
await websocket.close() await websocket.close()
# do some cleanup # do some cleanup
if isinstance((driver := get_driver()), ASGIMixin): if isinstance((driver := get_driver()), ASGIMixin):
driver.setup_websocket_server( driver.setup_websocket_server(
WebSocketServerSetup( WebSocketServerSetup(
@@ -129,6 +133,7 @@ from fastapi import FastAPI
app: FastAPI = nonebot.get_app() app: FastAPI = nonebot.get_app()
@app.get("/api") @app.get("/api")
async def custom_api(): async def custom_api():
return {"message": "Hello, world!"} return {"message": "Hello, world!"}
@@ -29,6 +29,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_startup @driver.on_startup
async def do_something(): async def do_something():
pass pass
@@ -43,6 +44,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_shutdown @driver.on_shutdown
async def do_something(): async def do_something():
pass pass
@@ -57,6 +59,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_bot_connect @driver.on_bot_connect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -71,6 +74,7 @@ from nonebot import get_driver
driver = get_driver() driver = get_driver()
@driver.on_bot_disconnect @driver.on_bot_disconnect
async def do_something(bot: Bot): async def do_something(bot: Bot):
pass pass
@@ -88,6 +92,7 @@ async def do_something(bot: Bot):
from nonebot.exception import IgnoredException from nonebot.exception import IgnoredException
from nonebot.message import event_preprocessor from nonebot.message import event_preprocessor
@event_preprocessor @event_preprocessor
async def do_something(event: Event): async def do_something(event: Event):
if not event.is_tome(): if not event.is_tome():
@@ -101,6 +106,7 @@ async def do_something(event: Event):
```python ```python
from nonebot.message import event_postprocessor from nonebot.message import event_postprocessor
@event_postprocessor @event_postprocessor
async def do_something(event: Event): async def do_something(event: Event):
pass pass
@@ -114,6 +120,7 @@ async def do_something(event: Event):
from nonebot.message import run_preprocessor from nonebot.message import run_preprocessor
from nonebot.exception import IgnoredException from nonebot.exception import IgnoredException
@run_preprocessor @run_preprocessor
async def do_something(event: Event, matcher: Matcher): async def do_something(event: Event, matcher: Matcher):
if not event.is_tome(): if not event.is_tome():
@@ -127,6 +134,7 @@ async def do_something(event: Event, matcher: Matcher):
```python ```python
from nonebot.message import run_postprocessor from nonebot.message import run_postprocessor
@run_postprocessor @run_postprocessor
async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]): async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]):
pass pass
@@ -140,6 +148,7 @@ async def do_something(event: Event, matcher: Matcher, exception: Optional[Excep
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
@Bot.on_calling_api @Bot.on_calling_api
async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):
if api == "send_msg": if api == "send_msg":
@@ -154,9 +163,14 @@ async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]):
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.exception import MockApiException from nonebot.exception import MockApiException
@Bot.on_called_api @Bot.on_called_api
async def handle_api_result( async def handle_api_result(
bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any bot: Bot,
exception: Optional[Exception],
api: str,
data: Dict[str, Any],
result: Any,
): ):
if not exception and api == "send_msg": if not exception and api == "send_msg":
raise MockApiException(result={**result, "message_id": 123}) raise MockApiException(result={**result, "message_id": 123})
@@ -21,6 +21,7 @@ options:
```python {3-5} ```python {3-5}
foo = on_message() foo = on_message()
@foo.type_updater @foo.type_updater
async def _() -> str: async def _() -> str:
return "notice" return "notice"
@@ -37,6 +38,7 @@ from nonebot.permission import User
foo = on_message() foo = on_message()
@foo.permission_updater @foo.permission_updater
async def _(event: Event, matcher: Matcher) -> Permission: async def _(event: Event, matcher: Matcher) -> Permission:
return Permission(User.from_event(event, perm=matcher.permission)) return Permission(User.from_event(event, perm=matcher.permission))
@@ -49,6 +51,7 @@ from nonebot.permission import USER
foo = on_message() foo = on_message()
@foo.permission_updater @foo.permission_updater
async def _(matcher: Matcher) -> Permission: async def _(matcher: Matcher) -> Permission:
return USER("session1", "session2", perm=matcher.permission) return USER("session1", "session2", perm=matcher.permission)
@@ -119,6 +119,8 @@ NoneBotException
```python ```python
matcher = on_notice(block=True) matcher = on_notice(block=True)
# 或者 # 或者
@matcher.handle() @matcher.handle()
async def handler(matcher: Matcher): async def handler(matcher: Matcher):
@@ -100,6 +100,7 @@ description: nonebot 模块
```python ```python
from nonebot.adapters.console import Adapter from nonebot.adapters.console import Adapter
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
``` ```
@@ -142,17 +142,18 @@ description: nonebot.params 模块
def depend_func() -> Any: def depend_func() -> Any:
return ... return ...
def depend_gen_func(): def depend_gen_func():
try: try:
yield ... yield ...
finally: finally:
... ...
async def handler( async def handler(
param_name: Any = Depends(depend_func), param_name: Any = Depends(depend_func),
gen: Any = Depends(depend_gen_func), gen: Any = Depends(depend_gen_func),
): ): ...
...
``` ```
## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam} ## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam}
@@ -76,7 +76,7 @@ logger.add(
level=0, level=0,
diagnose=True, diagnose=True,
format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}", format="<g>{time:MM-DD HH:mm:ss}</g> [<lvl>{level}</lvl>] <c><u>{name}</u></c> | {message}",
filter=default_filter filter=default_filter,
) )
``` ```
@@ -21,6 +21,7 @@ options:
```python {4} title=weather/__init__.py ```python {4} title=weather/__init__.py
from nonebot.adapters.console import MessageEvent from nonebot.adapters.console import MessageEvent
@weather.got("location", prompt="请输入地名") @weather.got("location", prompt="请输入地名")
async def got_location(event: MessageEvent, location: str = ArgPlainText()): async def got_location(event: MessageEvent, location: str = ArgPlainText()):
await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...") await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...")
@@ -39,10 +40,12 @@ async def got_location(event: MessageEvent, location: str = ArgPlainText()):
```python {4,8} ```python {4,8}
from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent
@matcher.handle() @matcher.handle()
async def handle_private(event: PrivateMessageEvent): async def handle_private(event: PrivateMessageEvent):
await matcher.finish("私聊消息") await matcher.finish("私聊消息")
@matcher.handle() @matcher.handle()
async def handle_group(event: GroupMessageEvent): async def handle_group(event: GroupMessageEvent):
await matcher.finish("群聊消息") await matcher.finish("群聊消息")
@@ -54,10 +57,12 @@ async def handle_group(event: GroupMessageEvent):
from nonebot.adapters.console import Bot as ConsoleBot from nonebot.adapters.console import Bot as ConsoleBot
from nonebot.adapters.onebot.v11 import Bot as OneBot from nonebot.adapters.onebot.v11 import Bot as OneBot
@matcher.handle() @matcher.handle()
async def handle_console(bot: ConsoleBot): async def handle_console(bot: ConsoleBot):
await bot.bell() await bot.bell()
@matcher.handle() @matcher.handle()
async def handle_onebot(bot: OneBot): async def handle_onebot(bot: OneBot):
await bot.send_group_message(group_id=123123, message="OneBot") await bot.send_group_message(group_id=123123, message="OneBot")
@@ -27,9 +27,11 @@ from .config import Config
plugin_config = get_plugin_config(Config) plugin_config = get_plugin_config(Config)
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
weather = on_command("天气", rule=is_enable) weather = on_command("天气", rule=is_enable)
``` ```
@@ -43,12 +45,15 @@ weather = on_command("天气", rule=is_enable)
from nonebot.rule import Rule from nonebot.rule import Rule
from nonebot.adapters import Event from nonebot.adapters import Event
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
async def is_blacklisted(event: Event) -> bool: async def is_blacklisted(event: Event) -> bool:
return event.get_user_id() not in BLACKLIST return event.get_user_id() not in BLACKLIST
rule = Rule(is_enable, is_blacklisted) rule = Rule(is_enable, is_blacklisted)
weather = on_command("天气", rule=rule) weather = on_command("天气", rule=rule)
@@ -66,9 +71,11 @@ from .config import Config
plugin_config = get_plugin_config(Config) plugin_config = get_plugin_config(Config)
async def is_enable() -> bool: async def is_enable() -> bool:
return plugin_config.weather_plugin_enabled return plugin_config.weather_plugin_enabled
weather = on_command( weather = on_command(
"天气", "天气",
rule=to_me() & is_enable, rule=to_me() & is_enable,
@@ -17,6 +17,7 @@ NoneBot 中的会话状态是一个字典,可以通过类型 `T_State` 来获
```python ```python
from nonebot.typing import T_State from nonebot.typing import T_State
@matcher.got("key", prompt="请输入密码") @matcher.got("key", prompt="请输入密码")
async def _(state: T_State, key: str = ArgPlainText()): async def _(state: T_State, key: str = ArgPlainText()):
if key != "some password": if key != "some password":
@@ -34,10 +35,12 @@ async def _(state: T_State, key: str = ArgPlainText()):
```python ```python
from nonebot.typing import T_State from nonebot.typing import T_State
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
state["key"] = "value" state["key"] = "value"
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
await matcher.finish(state["key"]) await matcher.finish(state["key"])
@@ -49,10 +52,12 @@ async def _(state: T_State):
from nonebot.typing import T_State from nonebot.typing import T_State
from nonebot.adapters import MessageTemplate from nonebot.adapters import MessageTemplate
@matcher.handle() @matcher.handle()
async def _(state: T_State): async def _(state: T_State):
state["username"] = "user" state["username"] = "user"
@matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码")) @matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码"))
async def _(): async def _():
await matcher.finish(MessageTemplate("密码为 {password}")) await matcher.finish(MessageTemplate("密码为 {password}"))
@@ -20,7 +20,7 @@ alc = Alconna(
Args["package", str], Args["package", str],
Option("-r|--requirement", Args["file", str]), Option("-r|--requirement", Args["file", str]),
Option("-i|--index-url", Args["url", str]), Option("-i|--index-url", Args["url", str]),
) ),
) )
res = alc.parse("pip install nonebot2 -i URL") res = alc.parse("pip install nonebot2 -i URL")
@@ -383,20 +383,33 @@ alc = Alconna(..., meta=CommandMeta("foo", example="bar"))
from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config
ns = Namespace("foo", prefixes=["/"]) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ ns = Namespace(
"foo", prefixes=["/"]
) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/
alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 alc = Alconna(
"pip", Subcommand("install", Args["package", str]), namespace=ns
) # 在创建Alconna时候传入命名空间以替换默认命名空间
# 可以通过with方式创建命名空间 # 可以通过with方式创建命名空间
with namespace("bar") as np1: with namespace("bar") as np1:
np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 np1.prefixes = [
"!"
] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令
np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter
np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 np1.builtin_option_name["help"] = {
"帮助",
"-h",
} # 设置此命名空间下的命令的帮助选项名称
# 你还可以使用config来管理所有命名空间并切换至任意命名空间 # 你还可以使用config来管理所有命名空间并切换至任意命名空间
config.namespaces["foo"] = ns # 将命名空间挂载到 config 上 config.namespaces["foo"] = ns # 将命名空间挂载到 config 上
alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间 alc = Alconna(
"pip",
Subcommand("install", Args["package", str]),
namespace=config.namespaces["foo"],
) # 也是同样可以切换到"foo"命名空间
``` ```
### 修改默认的命名空间 ### 修改默认的命名空间
@@ -469,10 +482,12 @@ alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"})
alc.shortcut("echo", delete=True) # 删除快捷指令 alc.shortcut("echo", delete=True) # 删除快捷指令
# 'Alconna::eval 的快捷指令: "echo" 删除成功' # 'Alconna::eval 的快捷指令: "echo" 删除成功'
@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器 @alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器
def cb(content: str): def cb(content: str):
eval(content, {}, {}) eval(content, {}, {})
alc.parse('eval print(\\"hello world\\")') alc.parse('eval print(\\"hello world\\")')
# hello world # hello world
@@ -523,7 +538,12 @@ alc.parse("eval --shortcut list")
from arclet.alconna import Alconna, Option, CommandMeta, Args from arclet.alconna import Alconna, Option, CommandMeta, Args
alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) alc = Alconna(
"test",
Args["foo", int],
Option("BAR", Args["baz", str], compact=True),
meta=CommandMeta(compact=True),
)
assert alc.parse("test123 BARabc").matched assert alc.parse("test123 BARabc").matched
``` ```
@@ -534,7 +554,9 @@ assert alc.parse("test123 BARabc").matched
from arclet.alconna import Alconna, Option, Args, append from arclet.alconna import Alconna, Option, Args, append
alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) alc = Alconna(
"gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)
)
print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content"))
# ['abc', 'def', 'xyz'] # ['abc', 'def', 'xyz']
``` ```
@@ -577,7 +599,7 @@ from arclet.alconna import Alconna, Args, Option
alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar")
alc.parse("test --comp") alc.parse("test --comp")
''' """
output output
以下是建议的输入: 以下是建议的输入:
@@ -588,7 +610,7 @@ output
* --shortcut * --shortcut
* foo * foo
* bar * bar
''' """
``` ```
## Duplication ## Duplication
@@ -600,7 +622,16 @@ output
以pip为例,其对应的 Duplication 应如下构造: 以pip为例,其对应的 Duplication 应如下构造:
```python ```python
from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count from arclet.alconna import (
Alconna,
Args,
Option,
OptionResult,
Duplication,
SubcommandStub,
Subcommand,
count,
)
class MyDup(Duplication): class MyDup(Duplication):
@@ -29,10 +29,10 @@ from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match
echo = on_alconna(Alconna("echo", Args["msg", str])) echo = on_alconna(Alconna("echo", Args["msg", str]))
@echo.handle() @echo.handle()
async def echo_exit(msg: Match[str] = AlconnaMatch("msg")): async def echo_exit(msg: Match[str] = AlconnaMatch("msg")):
await echo.finish(msg.result) await echo.finish(msg.result)
``` ```
相比于 `on_alconna` `funcommand` 增加了三个参数 `name`, `prefixes``description` 相比于 `on_alconna` `funcommand` 增加了三个参数 `name`, `prefixes``description`
@@ -54,6 +54,7 @@ book = (
.build() .build()
) )
@book.handle() @book.handle()
async def _(arp: Arparma): async def _(arp: Arparma):
await book.send(str(arp.options)) await book.send(str(arp.options))
@@ -45,14 +45,11 @@ message = UniMessage(
```python ```python
from nonebot_plugin_alconna import Button, UniMessage from nonebot_plugin_alconna import Button, UniMessage
message = ( message = UniMessage.text("hello world").keyboard(
UniMessage.text("hello world")
.keyboard(
Button("link1", url="https://example.com/1"), Button("link1", url="https://example.com/1"),
Button("link2", url="https://example.com/2"), Button("link2", url="https://example.com/2"),
Button("link3", url="https://example.com/3"), Button("link3", url="https://example.com/3"),
row=3, row=3,
)
) )
``` ```
@@ -94,6 +91,7 @@ async def _():
```python ```python
from nonebot_plugin_alconna import message_recall, message_edit, message_reaction from nonebot_plugin_alconna import message_recall, message_edit, message_reaction
@matcher.handle() @matcher.handle()
async def _(): async def _():
await message_edit(UniMessage.text("hello world")) await message_edit(UniMessage.text("hello world"))
@@ -120,9 +118,9 @@ async def _():
```python ```python
from nonebot_plugin_alconna import UniMsg from nonebot_plugin_alconna import UniMsg
@matcher.handle() @matcher.handle()
async def _(msg: UniMsg): async def _(msg: UniMsg): ...
...
``` ```
然后你可以通过 `UniMessage` 的方法来处理消息. 然后你可以通过 `UniMessage` 的方法来处理消息.
@@ -182,6 +180,7 @@ async def _(msg: UniMsg):
```python ```python
from nonebot_plugin_alconna import UniMsg from nonebot_plugin_alconna import UniMsg
@matcher.handle() @matcher.handle()
async def _(msg: UniMsg): async def _(msg: UniMsg):
data: list[dict] = msg.dump() data: list[dict] = msg.dump()
@@ -193,6 +192,7 @@ async def _(msg: UniMsg):
```python ```python
from nonebot_plugin_alconna import UniMessage from nonebot_plugin_alconna import UniMessage
@matcher.handle() @matcher.handle()
async def _(): async def _():
data = [ data = [
@@ -12,9 +12,9 @@ from nonebot_plugin_alconna import Alconna, Args, Image, on_alconna
meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image])) meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image]))
@meme.handle() @meme.handle()
async def _(img: Image): async def _(img: Image): ...
...
``` ```
## 模型定义 ## 模型定义
@@ -24,6 +24,7 @@ async def _(img: Image):
```python ```python
class Segment: class Segment:
"""基类标注""" """基类标注"""
@property @property
def type(self) -> str: ... def type(self) -> str: ...
@property @property
@@ -31,29 +32,40 @@ class Segment:
@property @property
def children(self) -> list["Segment"]: ... def children(self) -> list["Segment"]: ...
class Text(Segment): class Text(Segment):
"""Text对象, 表示一类文本元素""" """Text对象, 表示一类文本元素"""
text: str text: str
styles: dict[tuple[int, int], list[str]] styles: dict[tuple[int, int], list[str]]
def cover(self, text: str): ... def cover(self, text: str): ...
def mark(self, start: Optional[int] = None, end: Optional[int] = None, *styles: str): ... def mark(
self, start: Optional[int] = None, end: Optional[int] = None, *styles: str
): ...
class At(Segment): class At(Segment):
"""At对象, 表示一类提醒某用户的元素""" """At对象, 表示一类提醒某用户的元素"""
flag: Literal["user", "role", "channel"] flag: Literal["user", "role", "channel"]
target: str target: str
display: Optional[str] display: Optional[str]
class AtAll(Segment): class AtAll(Segment):
"""AtAll对象, 表示一类提醒所有人的元素""" """AtAll对象, 表示一类提醒所有人的元素"""
here: bool here: bool
class Emoji(Segment): class Emoji(Segment):
"""Emoji对象, 表示一类表情元素""" """Emoji对象, 表示一类表情元素"""
id: str id: str
name: Optional[str] name: Optional[str]
class Media(Segment): class Media(Segment):
id: Optional[str] id: Optional[str]
url: Optional[str] url: Optional[str]
@@ -64,53 +76,72 @@ class Media(Segment):
to_url: ClassVar[Optional[MediaToUrl]] to_url: ClassVar[Optional[MediaToUrl]]
class Image(Media): class Image(Media):
"""Image对象, 表示一类图片元素""" """Image对象, 表示一类图片元素"""
width: Optional[int] width: Optional[int]
height: Optional[int] height: Optional[int]
class Audio(Media): class Audio(Media):
"""Audio对象, 表示一类音频元素""" """Audio对象, 表示一类音频元素"""
duration: Optional[float] duration: Optional[float]
class Voice(Media): class Voice(Media):
"""Voice对象, 表示一类语音元素""" """Voice对象, 表示一类语音元素"""
duration: Optional[float] duration: Optional[float]
class Video(Media): class Video(Media):
"""Video对象, 表示一类视频元素""" """Video对象, 表示一类视频元素"""
thumbnail: Optional[Image] thumbnail: Optional[Image]
duration: Optional[float] duration: Optional[float]
class File(Media): class File(Media):
"""File对象, 表示一类文件元素""" """File对象, 表示一类文件元素"""
class Reply(Segment): class Reply(Segment):
"""Reply对象,表示一类回复消息""" """Reply对象,表示一类回复消息"""
id: str id: str
"""此处不一定是消息ID,可能是其他ID,如消息序号等""" """此处不一定是消息ID,可能是其他ID,如消息序号等"""
msg: Optional[Union[Message, str]] msg: Optional[Union[Message, str]]
origin: Optional[Any] origin: Optional[Any]
class Reference(Segment): class Reference(Segment):
"""Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类""" """Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类"""
id: Optional[str] id: Optional[str]
"""此处不一定是消息ID,可能是其他ID,如消息序号等""" """此处不一定是消息ID,可能是其他ID,如消息序号等"""
children: List[Union[RefNode, CustomNode]] children: List[Union[RefNode, CustomNode]]
class Hyper(Segment): class Hyper(Segment):
"""Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等""" """Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等"""
format: Literal["xml", "json"] format: Literal["xml", "json"]
raw: Optional[str] raw: Optional[str]
content: Optional[Union[dict, list]] content: Optional[Union[dict, list]]
class Reference(Segment): class Reference(Segment):
"""Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类""" """Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类"""
id: Optional[str] id: Optional[str]
nodes: Sequence[Union[RefNode, CustomNode]] nodes: Sequence[Union[RefNode, CustomNode]]
class Button(Segment): class Button(Segment):
"""Button对象,表示一类按钮消息""" """Button对象,表示一类按钮消息"""
flag: Literal["action", "link", "input", "enter"] flag: Literal["action", "link", "input", "enter"]
""" """
- 点击 action 类型的按钮时会触发一个关于 按钮回调 事件,该事件的 button 资源会包含上述 id - 点击 action 类型的按钮时会触发一个关于 按钮回调 事件,该事件的 button 资源会包含上述 id
@@ -138,20 +169,26 @@ class Button(Segment):
- list[At]: 指定用户/身份组可操作 - list[At]: 指定用户/身份组可操作
""" """
class Keyboard(Segment): class Keyboard(Segment):
"""Keyboard对象,表示一行按钮元素""" """Keyboard对象,表示一行按钮元素"""
id: Optional[str] id: Optional[str]
"""此处一般用来表示模板id,特殊情况下可能表示例如 bot_appid 等""" """此处一般用来表示模板id,特殊情况下可能表示例如 bot_appid 等"""
buttons: Optional[list[Button]] buttons: Optional[list[Button]]
row: Optional[int] row: Optional[int]
"""当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数""" """当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数"""
class Other(Segment): class Other(Segment):
"""其他 Segment""" """其他 Segment"""
origin: MessageSegment origin: MessageSegment
class I18n(Segment): class I18n(Segment):
"""特殊的 Segment,用于 i18n 消息""" """特殊的 Segment,用于 i18n 消息"""
item_or_scope: Union[LangItem, str] item_or_scope: Union[LangItem, str]
type_: Optional[str] = None type_: Optional[str] = None
@@ -172,10 +209,14 @@ from nonebot_plugin_alconna import Args, Image, Alconna, select
from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace
# 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果 # 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果
alc1 = Alconna("make_meme", Args["name", str]["img", select(Image).first]) # 也可以使用 select(Image).nth(0) alc1 = Alconna(
"make_meme", Args["name", str]["img", select(Image).first]
) # 也可以使用 select(Image).nth(0)
# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image # 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image
alc2 = Alconna("make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]]) alc2 = Alconna(
"make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]]
)
``` ```
也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取) 也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取)
@@ -213,10 +254,13 @@ def mfbuild(builder: MessageBuilder, seg: BaseMessageSegment):
@custom_handler(MarketFace) @custom_handler(MarketFace)
async def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool): async def mfexport(
exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool
):
if exporter.get_message_type() is Message: if exporter.get_message_type() is Message:
return MessageSegment("chronocat:marketface", seg.data)(await exporter.export(seg.children, bot, fallback)) return MessageSegment("chronocat:marketface", seg.data)(
await exporter.export(seg.children, bot, fallback)
)
``` ```
具体而言,你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。 具体而言,你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。
@@ -155,7 +155,9 @@ op.create_table( # CREATE TABLE
"weather_weather", # weather_weather "weather_weather", # weather_weather
sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL, sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL,
sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL, sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL,
sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), # CONSTRAINT pk_weather_weather PRIMARY KEY (location) sa.PrimaryKeyConstraint(
"location", name=op.f("pk_weather_weather")
), # CONSTRAINT pk_weather_weather PRIMARY KEY (location)
info={"bind_key": "weather"}, info={"bind_key": "weather"},
) )
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -245,7 +247,9 @@ from nonebot.typing import T_State
@weather.got("location", prompt="请输入地名") @weather.got("location", prompt="请输入地名")
async def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()): async def _(
state: T_State, session: async_scoped_session, location: str = ArgPlainText()
):
wea = await session.get(Weather, location) wea = await session.get(Weather, location)
if not wea: if not wea:
@@ -348,13 +352,16 @@ async def _(
```python title=weather/__init__.py {5} showLineNumbers ```python title=weather/__init__.py {5} showLineNumbers
from collections.abc import Sequence from collections.abc import Sequence
@weather.handle() @weather.handle()
async def _( async def _(
weas: Sequence[Weather] = SQLDepends( weas: Sequence[Weather] = SQLDepends(
select(Weather).where(Weather.weather == Depends(extract_arg_plain_text)) select(Weather).where(Weather.weather == Depends(extract_arg_plain_text))
), ),
): ):
await weather.send(f"今天的天气是{weas[0].weather}的城市有{''.join(wea.location for wea in weas)}") await weather.send(
f"今天的天气是{weas[0].weather}的城市有{''.join(wea.location for wea in weas)}"
)
``` ```
支持的类型标注请参见 [依赖注入](dependency)。 支持的类型标注请参见 [依赖注入](dependency)。
@@ -364,6 +371,7 @@ async def _(
```python title=weather/__init__.py {5-6,10} showLineNumbers ```python title=weather/__init__.py {5-6,10} showLineNumbers
from collections.abc import Sequence from collections.abc import Sequence
class Weather(Model): class Weather(Model):
location: Mapped[str] = mapped_column(primary_key=True) location: Mapped[str] = mapped_column(primary_key=True)
weather: Mapped[str] = Depends(extract_arg_plain_text) weather: Mapped[str] = Depends(extract_arg_plain_text)
@@ -78,8 +78,7 @@ async def html_to_pic(
img_fetch_fn: ImgFetchFn = combined_img_fetcher, img_fetch_fn: ImgFetchFn = combined_img_fetcher,
css_fetch_fn: CSSFetchFn = combined_css_fetcher, css_fetch_fn: CSSFetchFn = combined_css_fetcher,
urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin, urljoin_fn: Callable[[str, str], str] = urllib3.parse.urljoin,
) -> bytes: ) -> bytes: ...
...
``` ```
最核心的渲染函数。 最核心的渲染函数。
@@ -107,8 +106,7 @@ async def text_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
可用于渲染多行文本。 可用于渲染多行文本。
@@ -128,8 +126,7 @@ async def md_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。 可用于渲染 Markdown 文本。默认为 GitHub Markdown Light 风格,支持基于 `pygments` 的代码高亮。
@@ -153,8 +150,7 @@ async def template_to_pic(
allow_refit: bool = True, allow_refit: bool = True,
image_format: Literal["png", "jpeg"] = "png", image_format: Literal["png", "jpeg"] = "png",
jpeg_quality: int = 100, jpeg_quality: int = 100,
) -> bytes: ) -> bytes: ...
...
``` ```
渲染 jinja2 模板。 渲染 jinja2 模板。
@@ -44,15 +44,18 @@ require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_apscheduler import scheduler
# 基于装饰器的方式 # 基于装饰器的方式
@scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2}) @scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2})
async def run_every_2_hour(arg1: int, arg2: int): async def run_every_2_hour(arg1: int, arg2: int):
pass pass
# 基于 add_job 方法的方式 # 基于 add_job 方法的方式
def run_every_day(arg1: int, arg2: int): def run_every_day(arg1: int, arg2: int):
pass pass
scheduler.add_job( scheduler.add_job(
run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2} run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2}
) )
@@ -25,6 +25,7 @@ NoneBot 中的网络通信主要包括以下几种:
```python {5,6} title=tests/test_http_server.py ```python {5,6} title=tests/test_http_server.py
from nonebug import App from nonebug import App
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_http_server(app: App): async def test_http_server(app: App):
async with app.test_server() as ctx: async with app.test_server() as ctx:
@@ -45,6 +46,7 @@ import nonebot
from nonebug import App from nonebug import App
from nonebot.adapters.fake import Adapter from nonebot.adapters.fake import Adapter
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_http_server(app: App): async def test_http_server(app: App):
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
@@ -72,6 +74,7 @@ import nonebot
from nonebug import App from nonebug import App
from nonebot.adapters.fake import Adapter from nonebot.adapters.fake import Adapter
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_ws_server(app: App): async def test_ws_server(app: App):
adapter = nonebot.get_adapter(Adapter) adapter = nonebot.get_adapter(Adapter)
@@ -81,6 +81,7 @@ except Exception as e:
```python title=config.py ```python title=config.py
from pydantic import BaseModel from pydantic import BaseModel
class Config(BaseModel): class Config(BaseModel):
xxx_id: str xxx_id: str
xxx_token: str xxx_token: str
@@ -102,6 +103,7 @@ from nonebot.adapters import Adapter as BaseAdapter
from .config import Config from .config import Config
class Adapter(BaseAdapter): class Adapter(BaseAdapter):
@override @override
def __init__(self, driver: Driver, **kwargs: Any): def __init__(self, driver: Driver, **kwargs: Any):
@@ -208,9 +210,10 @@ from nonebot.drivers import (
ASGIMixin, ASGIMixin,
WebSocket, WebSocket,
HTTPServerSetup, HTTPServerSetup,
WebSocketServerSetup WebSocketServerSetup,
) )
class Adapter(BaseAdapter): class Adapter(BaseAdapter):
@override @override
def __init__(self, driver: Driver, **kwargs: Any): def __init__(self, driver: Driver, **kwargs: Any):
@@ -242,7 +245,6 @@ class Adapter(BaseAdapter):
) )
self.setup_websocket_server(ws_setup) self.setup_websocket_server(ws_setup)
async def _handle_http(self, request: Request) -> Response: async def _handle_http(self, request: Request) -> Response:
"""HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response""" """HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response"""
... ...
@@ -270,8 +272,8 @@ class Adapter(BaseAdapter):
```python {7,8,11} title=adapter.py ```python {7,8,11} title=adapter.py
from .bot import Bot from .bot import Bot
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
def _handle_connect(self): def _handle_connect(self):
bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID
bot = Bot(self, self_id=bot_id) # 实例化 Bot bot = Bot(self, self_id=bot_id) # 实例化 Bot
@@ -295,8 +297,8 @@ from .bot import Bot
from .event import Event from .event import Event
from .log import log from .log import log
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
@classmethod @classmethod
def payload_to_event(cls, payload: Dict[str, Any]) -> Event: def payload_to_event(cls, payload: Dict[str, Any]) -> Event:
"""根据平台事件的特性,转换平台 payload 为具体 Event """根据平台事件的特性,转换平台 payload 为具体 Event
@@ -316,7 +318,6 @@ class Adapter(BaseAdapter):
# 也可以尝试转为基础 Event 进行处理 # 也可以尝试转为基础 Event 进行处理
return type_validate_python(Event, payload) return type_validate_python(Event, payload)
async def _forward(self, bot: Bot): async def _forward(self, bot: Bot):
payload: Dict[str, Any] # 接收到的事件数据 payload: Dict[str, Any] # 接收到的事件数据
@@ -337,8 +338,8 @@ from nonebot.drivers import Request, WebSocket
from .bot import Bot from .bot import Bot
class Adapter(BaseAdapter):
class Adapter(BaseAdapter):
@override @override
async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any:
log("DEBUG", f"Calling API <y>{api}</y>") # 给予日志提示 log("DEBUG", f"Calling API <y>{api}</y>") # 给予日志提示
@@ -356,7 +357,6 @@ class Adapter(BaseAdapter):
# 发送请求,返回结果 # 发送请求,返回结果
return await self.driver.request(request) return await self.driver.request(request)
# 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据 # 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据
# 通过某种方式获取到 bot 对应的 websocket 对象 # 通过某种方式获取到 bot 对应的 websocket 对象
ws: WebSocket = your_get_websocket_method(bot.self_id) ws: WebSocket = your_get_websocket_method(bot.self_id)
@@ -450,8 +450,8 @@ from typing_extensions import override
from nonebot.compat import model_dump from nonebot.compat import model_dump
from nonebot.adapters import Event as BaseEvent from nonebot.adapters import Event as BaseEvent
class Event(BaseEvent):
class Event(BaseEvent):
@override @override
def get_event_name(self) -> str: def get_event_name(self) -> str:
# 返回事件的名称,用于日志打印 # 返回事件的名称,用于日志打印
@@ -488,6 +488,7 @@ class Event(BaseEvent):
```python {7,16,20,25,34,42} title=event.py ```python {7,16,20,25,34,42} title=event.py
from .message import Message from .message import Message
class HeartbeatEvent(Event): class HeartbeatEvent(Event):
"""心跳时间,通常为元事件""" """心跳时间,通常为元事件"""
@@ -495,8 +496,10 @@ class HeartbeatEvent(Event):
def get_type(self) -> str: def get_type(self) -> str:
return "meta_event" return "meta_event"
class MessageEvent(Event): class MessageEvent(Event):
"""消息事件""" """消息事件"""
message_id: str message_id: str
user_id: str user_id: str
@@ -513,8 +516,10 @@ class MessageEvent(Event):
def get_user_id(self) -> str: def get_user_id(self) -> str:
return self.user_id return self.user_id
class JoinRoomEvent(Event): class JoinRoomEvent(Event):
"""加入房间事件,通常为通知事件""" """加入房间事件,通常为通知事件"""
user_id: str user_id: str
room_id: str room_id: str
@@ -522,8 +527,10 @@ class JoinRoomEvent(Event):
def get_type(self) -> str: def get_type(self) -> str:
return "notice" return "notice"
class ApplyAddFriendEvent(Event): class ApplyAddFriendEvent(Event):
"""申请添加好友事件,通常为请求事件""" """申请添加好友事件,通常为请求事件"""
user_id: str user_id: str
@override @override
@@ -544,6 +551,7 @@ from nonebot.utils import escape_tag
from nonebot.adapters import Message as BaseMessage from nonebot.adapters import Message as BaseMessage
from nonebot.adapters import MessageSegment as BaseMessageSegment from nonebot.adapters import MessageSegment as BaseMessageSegment
class MessageSegment(BaseMessageSegment["Message"]): class MessageSegment(BaseMessageSegment["Message"]):
@classmethod @classmethod
@override @override
@@ -591,6 +599,7 @@ class Message(BaseMessage[MessageSegment]):
```python title=tests/conftest.py ```python title=tests/conftest.py
from pathlib import Path from pathlib import Path
import nonebot.adapters import nonebot.adapters
nonebot.adapters.__path__.append( # type: ignore nonebot.adapters.__path__.append( # type: ignore
str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve()) str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve())
) )
@@ -48,7 +48,9 @@ weather = on_command("天气")
from nonebot import on_command from nonebot import on_command
from nonebot.rule import to_me from nonebot.rule import to_me
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) weather = on_command(
"天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True
)
``` ```
这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。 这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。
@@ -174,9 +174,7 @@ message = Message(
# 索引 # 索引
message[0] == MessageSegment.text("test") message[0] == MessageSegment.text("test")
# 切片 # 切片
message[0:2] == Message( message[0:2] == Message([MessageSegment.text("test"), MessageSegment.markdown("test2")])
[MessageSegment.text("test"), MessageSegment.markdown("test2")]
)
# 类型过滤 # 类型过滤
message["markdown"] == Message( message["markdown"] == Message(
[MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")]
@@ -262,7 +260,7 @@ msg = seg.join(
MessageSegment.text("second"), MessageSegment.text("second"),
MessageSegment.text("third"), MessageSegment.text("third"),
] ]
) ),
] ]
) )
msg == Message( msg == Message(