🔀 Merge pull request #771

Feature: refactor and support bot connection hook
This commit is contained in:
Ju4tCode 2022-02-06 18:16:18 +08:00 committed by GitHub
commit b90054e61b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1840 additions and 1717 deletions

View File

@ -122,16 +122,16 @@ def get_asgi() -> Any:
def get_bot(self_id: Optional[str] = None) -> Bot: def get_bot(self_id: Optional[str] = None) -> Bot:
"""获取一个连接到 NoneBot 的 {ref}`nonebot.adapters._bot.Bot` 对象。 """获取一个连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
当提供 `self_id` 此函数是 `get_bots()[self_id]` 的简写 当提供 `self_id` 此函数是 `get_bots()[self_id]` 的简写
当不提供时返回一个 {ref}`nonebot.adapters._bot.Bot` 当不提供时返回一个 {ref}`nonebot.adapters.Bot`
参数: 参数:
self_id: 用来识别 {ref}`nonebot.adapters._bot.Bot` {ref}`nonebot.adapters._bot.Bot.self_id` 属性 self_id: 用来识别 {ref}`nonebot.adapters.Bot` {ref}`nonebot.adapters.Bot.self_id` 属性
返回: 返回:
{ref}`nonebot.adapters._bot.Bot` 对象 {ref}`nonebot.adapters.Bot` 对象
异常: 异常:
KeyError: 对应 self_id Bot 不存在 KeyError: 对应 self_id Bot 不存在
@ -156,10 +156,10 @@ def get_bot(self_id: Optional[str] = None) -> Bot:
def get_bots() -> Dict[str, Bot]: def get_bots() -> Dict[str, Bot]:
"""获取所有连接到 NoneBot 的 {ref}`nonebot.adapters._bot.Bot` 对象。 """获取所有连接到 NoneBot 的 {ref}`nonebot.adapters.Bot` 对象。
返回: 返回:
一个以 {ref}`nonebot.adapters._bot.Bot.self_id` 为键{ref}`nonebot.adapters._bot.Bot` 对象为值的字典 一个以 {ref}`nonebot.adapters.Bot.self_id` 为键{ref}`nonebot.adapters.Bot` 对象为值的字典
异常: 异常:
ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用) ValueError: 全局 {ref}`nonebot.drivers.Driver` 对象尚未初始化 ({ref}`nonebot.init <nonebot.init>` 尚未调用)
@ -258,7 +258,6 @@ def run(*args: Any, **kwargs: Any) -> None:
get_driver().run(*args, **kwargs) get_driver().run(*args, **kwargs)
import nonebot.params as params
from nonebot.plugin import on as on from nonebot.plugin import on as on
from nonebot.plugin import export as export from nonebot.plugin import export as export
from nonebot.plugin import require as require from nonebot.plugin import require as require
@ -283,3 +282,5 @@ from nonebot.plugin import on_shell_command as on_shell_command
from nonebot.plugin import get_loaded_plugins as get_loaded_plugins from nonebot.plugin import get_loaded_plugins as get_loaded_plugins
from nonebot.plugin import load_builtin_plugin as load_builtin_plugin from nonebot.plugin import load_builtin_plugin as load_builtin_plugin
from nonebot.plugin import load_builtin_plugins as load_builtin_plugins from nonebot.plugin import load_builtin_plugins as load_builtin_plugins
__autodoc__ = {"internal": False}

View File

@ -22,17 +22,18 @@ except ImportError:
except Exception: except Exception:
pass pass
from ._bot import Bot as Bot from nonebot.internal.adapter import Bot as Bot
from ._event import Event as Event from nonebot.internal.adapter import Event as Event
from ._adapter import Adapter as Adapter from nonebot.internal.adapter import Adapter as Adapter
from ._message import Message as Message from nonebot.internal.adapter import Message as Message
from ._message import MessageSegment as MessageSegment from nonebot.internal.adapter import MessageSegment as MessageSegment
from ._template import MessageTemplate as MessageTemplate from nonebot.internal.adapter import MessageTemplate as MessageTemplate
__autodoc__ = { __autodoc__ = {
"_bot": True, "Bot": True,
"_event": True, "Event": True,
"_adapter": True, "Adapter": True,
"_message": True, "Message": True,
"_template": True, "MessageSegment": True,
"MessageTemplate": True,
} }

View File

@ -7,259 +7,32 @@ FrontMatter:
description: nonebot.drivers 模块 description: nonebot.drivers 模块
""" """
import abc from nonebot.internal.driver import URL as URL
import asyncio from nonebot.internal.driver import Driver as Driver
from dataclasses import dataclass from nonebot.internal.driver import Cookies as Cookies
from contextlib import asynccontextmanager from nonebot.internal.driver import Request as Request
from typing import ( from nonebot.internal.driver import Response as Response
TYPE_CHECKING, from nonebot.internal.driver import WebSocket as WebSocket
Any, from nonebot.internal.driver import HTTPVersion as HTTPVersion
Set, from nonebot.internal.driver import ForwardMixin as ForwardMixin
Dict, from nonebot.internal.driver import ForwardDriver as ForwardDriver
Type, from nonebot.internal.driver import ReverseDriver as ReverseDriver
Callable, from nonebot.internal.driver import combine_driver as combine_driver
Awaitable, from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup
AsyncGenerator, from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup
)
from nonebot.log import logger __autodoc__ = {
from nonebot.utils import escape_tag "URL": True,
from nonebot.config import Env, Config "Driver": True,
from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook "Cookies": True,
"Request": True,
from ._model import URL as URL "Response": True,
from ._model import Request as Request "WebSocket": True,
from ._model import Response as Response "HTTPVersion": True,
from ._model import WebSocket as WebSocket "ForwardMixin": True,
from ._model import HTTPVersion as HTTPVersion "ForwardDriver": True,
"ReverseDriver": True,
if TYPE_CHECKING: "combine_driver": True,
from nonebot.adapters import Bot, Adapter "HTTPServerSetup": True,
"WebSocketServerSetup": True,
}
class Driver(abc.ABC):
"""Driver 基类。
参数:
env: 包含环境信息的 Env 对象
config: 包含配置信息的 Config 对象
"""
_adapters: Dict[str, "Adapter"] = {}
"""已注册的适配器列表"""
_bot_connection_hook: Set[T_BotConnectionHook] = set()
"""Bot 连接建立时执行的函数"""
_bot_disconnection_hook: Set[T_BotDisconnectionHook] = set()
"""Bot 连接断开时执行的函数"""
def __init__(self, env: Env, config: Config):
self.env: str = env.environment
"""环境名称"""
self.config: Config = config
"""全局配置对象"""
self._clients: Dict[str, "Bot"] = {}
@property
def bots(self) -> Dict[str, "Bot"]:
"""获取当前所有已连接的 Bot"""
return self._clients
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
"""注册一个协议适配器
参数:
adapter: 适配器类
kwargs: 其他传递给适配器的参数
"""
name = adapter.get_name()
if name in self._adapters:
logger.opt(colors=True).debug(
f'Adapter "<y>{escape_tag(name)}</y>" already exists'
)
return
self._adapters[name] = adapter(self, **kwargs)
logger.opt(colors=True).debug(
f'Succeeded to load adapter "<y>{escape_tag(name)}</y>"'
)
@property
@abc.abstractmethod
def type(self) -> str:
"""驱动类型名称"""
raise NotImplementedError
@property
@abc.abstractmethod
def logger(self):
"""驱动专属 logger 日志记录器"""
raise NotImplementedError
@abc.abstractmethod
def run(self, *args, **kwargs):
"""
启动驱动框架
"""
logger.opt(colors=True).debug(
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
)
@abc.abstractmethod
def on_startup(self, func: Callable) -> Callable:
"""注册一个在驱动器启动时执行的函数"""
raise NotImplementedError
@abc.abstractmethod
def on_shutdown(self, func: Callable) -> Callable:
"""注册一个在驱动器停止时执行的函数"""
raise NotImplementedError
def on_bot_connect(self, func: T_BotConnectionHook) -> T_BotConnectionHook:
"""装饰一个函数使他在 bot 连接成功时执行。
钩子函数参数:
- bot: 当前连接上的 Bot 对象
"""
self._bot_connection_hook.add(func)
return func
def on_bot_disconnect(self, func: T_BotDisconnectionHook) -> T_BotDisconnectionHook:
"""装饰一个函数使他在 bot 连接断开时执行。
钩子函数参数:
- bot: 当前连接上的 Bot 对象
"""
self._bot_disconnection_hook.add(func)
return func
def _bot_connect(self, bot: "Bot") -> None:
"""在连接成功后,调用该函数来注册 bot 对象"""
if bot.self_id in self._clients:
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
self._clients[bot.self_id] = bot
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot), self._bot_connection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
asyncio.create_task(_run_hook(bot))
def _bot_disconnect(self, bot: "Bot") -> None:
"""在连接断开后,调用该函数来注销 bot 对象"""
if bot.self_id in self._clients:
del self._clients[bot.self_id]
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot), self._bot_disconnection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
asyncio.create_task(_run_hook(bot))
class ForwardMixin(abc.ABC):
"""客户端混入基类。"""
@property
@abc.abstractmethod
def type(self) -> str:
"""客户端驱动类型名称"""
raise NotImplementedError
@abc.abstractmethod
async def request(self, setup: Request) -> Response:
"""发送一个 HTTP 请求"""
raise NotImplementedError
@abc.abstractmethod
@asynccontextmanager
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
"""发起一个 WebSocket 连接"""
raise NotImplementedError
yield # used for static type checking's generator detection
class ForwardDriver(Driver, ForwardMixin):
"""客户端基类。将客户端框架封装,以满足适配器使用。"""
class ReverseDriver(Driver):
"""服务端基类。将后端框架封装,以满足适配器使用。"""
@property
@abc.abstractmethod
def server_app(self) -> Any:
"""驱动 APP 对象"""
raise NotImplementedError
@property
@abc.abstractmethod
def asgi(self) -> Any:
"""驱动 ASGI 对象"""
raise NotImplementedError
@abc.abstractmethod
def setup_http_server(self, setup: "HTTPServerSetup") -> None:
"""设置一个 HTTP 服务器路由配置"""
raise NotImplementedError
@abc.abstractmethod
def setup_websocket_server(self, setup: "WebSocketServerSetup") -> None:
"""设置一个 WebSocket 服务器路由配置"""
raise NotImplementedError
def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Driver]:
"""将一个驱动器和多个混入类合并。"""
# check first
assert issubclass(driver, Driver), "`driver` must be subclass of Driver"
assert all(
map(lambda m: issubclass(m, ForwardMixin), mixins)
), "`mixins` must be subclass of ForwardMixin"
if not mixins:
return driver
class CombinedDriver(*mixins, driver, ForwardDriver): # type: ignore
@property
def type(self) -> str:
return (
driver.type.__get__(self)
+ "+"
+ "+".join(map(lambda x: x.type.__get__(self), mixins))
)
return CombinedDriver
@dataclass
class HTTPServerSetup:
"""HTTP 服务器路由配置。"""
path: URL # path should not be absolute, check it by URL.is_absolute() == False
method: str
name: str
handle_func: Callable[[Request], Awaitable[Response]]
@dataclass
class WebSocketServerSetup:
"""WebSocket 服务器路由配置。"""
path: URL # path should not be absolute, check it by URL.is_absolute() == False
name: str
handle_func: Callable[[WebSocket], Awaitable[Any]]

View File

@ -22,13 +22,12 @@ from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
from nonebot.config import Env from nonebot.config import Env
from nonebot.typing import overrides from nonebot.typing import overrides
from nonebot.exception import WebSocketClosed from nonebot.exception import WebSocketClosed
from nonebot.internal.driver import FileTypes
from nonebot.config import Config as NoneBotConfig from nonebot.config import Config as NoneBotConfig
from nonebot.drivers import Request as BaseRequest from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
from ._model import FileTypes
def catch_closed(func): def catch_closed(func):
@wraps(func) @wraps(func)

View File

@ -25,13 +25,12 @@ from pydantic import BaseSettings
from nonebot.config import Env from nonebot.config import Env
from nonebot.typing import overrides from nonebot.typing import overrides
from nonebot.exception import WebSocketClosed from nonebot.exception import WebSocketClosed
from nonebot.internal.driver import FileTypes
from nonebot.config import Config as NoneBotConfig from nonebot.config import Config as NoneBotConfig
from nonebot.drivers import Request as BaseRequest from nonebot.drivers import Request as BaseRequest
from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup
from ._model import FileTypes
try: try:
from quart import request as _request from quart import request as _request
from quart import websocket as _websocket from quart import websocket as _websocket

View File

@ -202,7 +202,7 @@ class AdapterException(NoneBotException):
class NoLogException(AdapterException): class NoLogException(AdapterException):
"""指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。 """指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。
可在 {ref}`nonebot.adapters._event.Event.get_log_string` 时抛出 可在 {ref}`nonebot.adapters.Event.get_log_string` 时抛出
""" """

View File

View File

@ -0,0 +1,6 @@
from .bot import Bot as Bot
from .event import Event as Event
from .adapter import Adapter as Adapter
from .message import Message as Message
from .message import MessageSegment as MessageSegment
from .template import MessageTemplate as MessageTemplate

View File

@ -1,14 +1,9 @@
"""
FrontMatter:
sidebar_position: 1
description: nonebot.adapters._adapter 模块
"""
import abc import abc
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Any, Dict, AsyncGenerator from typing import Any, Dict, AsyncGenerator
from nonebot.config import Config from nonebot.config import Config
from nonebot.drivers import ( from nonebot.internal.driver import (
Driver, Driver,
Request, Request,
Response, Response,
@ -19,7 +14,7 @@ from nonebot.drivers import (
WebSocketServerSetup, WebSocketServerSetup,
) )
from ._bot import Bot from .bot import Bot
class Adapter(abc.ABC): class Adapter(abc.ABC):
@ -36,7 +31,7 @@ class Adapter(abc.ABC):
self.driver: Driver = driver self.driver: Driver = driver
"""{ref}`nonebot.drivers.Driver` 实例""" """{ref}`nonebot.drivers.Driver` 实例"""
self.bots: Dict[str, Bot] = {} self.bots: Dict[str, Bot] = {}
"""本协议适配器已建立连接的 {ref}`nonebot.adapters._bot.Bot` 实例""" """本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例"""
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
@ -50,23 +45,23 @@ class Adapter(abc.ABC):
return self.driver.config return self.driver.config
def bot_connect(self, bot: Bot) -> None: def bot_connect(self, bot: Bot) -> None:
"""告知 NoneBot 建立了一个新的 {ref}`nonebot.adapters._bot.Bot` 连接。 """告知 NoneBot 建立了一个新的 {ref}`nonebot.adapters.Bot` 连接。
当有新的 {ref}`nonebot.adapters._bot.Bot` 实例连接建立成功时调用 当有新的 {ref}`nonebot.adapters.Bot` 实例连接建立成功时调用
参数: 参数:
bot: {ref}`nonebot.adapters._bot.Bot` 实例 bot: {ref}`nonebot.adapters.Bot` 实例
""" """
self.driver._bot_connect(bot) self.driver._bot_connect(bot)
self.bots[bot.self_id] = bot self.bots[bot.self_id] = bot
def bot_disconnect(self, bot: Bot) -> None: def bot_disconnect(self, bot: Bot) -> None:
"""告知 NoneBot {ref}`nonebot.adapters._bot.Bot` 连接已断开。 """告知 NoneBot {ref}`nonebot.adapters.Bot` 连接已断开。
当有 {ref}`nonebot.adapters._bot.Bot` 实例连接断开时调用 当有 {ref}`nonebot.adapters.Bot` 实例连接断开时调用
参数: 参数:
bot: {ref}`nonebot.adapters._bot.Bot` 实例 bot: {ref}`nonebot.adapters.Bot` 实例
""" """
self.driver._bot_disconnect(bot) self.driver._bot_disconnect(bot)
self.bots.pop(bot.self_id, None) self.bots.pop(bot.self_id, None)

View File

@ -1,8 +1,3 @@
"""
FrontMatter:
sidebar_position: 2
description: nonebot.adapters._bot 模块
"""
import abc import abc
import asyncio import asyncio
from functools import partial from functools import partial
@ -15,9 +10,9 @@ from nonebot.exception import MockApiException
from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook
if TYPE_CHECKING: if TYPE_CHECKING:
from ._event import Event from .event import Event
from ._adapter import Adapter from .adapter import Adapter
from ._message import Message, MessageSegment from .message import Message, MessageSegment
class _ApiCall(Protocol): class _ApiCall(Protocol):

View File

@ -1,15 +1,10 @@
"""
FrontMatter:
sidebar_position: 3
description: nonebot.adapters._event 模块
"""
import abc import abc
from pydantic import BaseModel from pydantic import BaseModel
from nonebot.utils import DataclassEncoder from nonebot.utils import DataclassEncoder
from ._message import Message from .message import Message
class Event(abc.ABC, BaseModel): class Event(abc.ABC, BaseModel):

View File

@ -1,8 +1,3 @@
"""
FrontMatter:
sidebar_position: 4
description: nonebot.adapters._message 模块
"""
import abc import abc
from copy import deepcopy from copy import deepcopy
from dataclasses import field, asdict, dataclass from dataclasses import field, asdict, dataclass
@ -22,7 +17,7 @@ from typing import (
from pydantic import parse_obj_as from pydantic import parse_obj_as
from ._template import MessageTemplate from .template import MessageTemplate
T = TypeVar("T") T = TypeVar("T")
TMS = TypeVar("TMS", bound="MessageSegment") TMS = TypeVar("TMS", bound="MessageSegment")

View File

@ -1,8 +1,3 @@
"""
FrontMatter:
sidebar_position: 5
description: nonebot.adapters._template 模块
"""
import inspect import inspect
import functools import functools
from string import Formatter from string import Formatter
@ -26,7 +21,7 @@ from typing import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from . import Message, MessageSegment from .message import Message, MessageSegment
TM = TypeVar("TM", bound="Message") TM = TypeVar("TM", bound="Message")
TF = TypeVar("TF", str, "Message") TF = TypeVar("TF", str, "Message")

View File

@ -0,0 +1,25 @@
from .model import URL as URL
from .model import RawURL as RawURL
from .driver import Driver as Driver
from .model import Cookies as Cookies
from .model import Request as Request
from .model import FileType as FileType
from .model import Response as Response
from .model import DataTypes as DataTypes
from .model import FileTypes as FileTypes
from .model import WebSocket as WebSocket
from .model import FilesTypes as FilesTypes
from .model import QueryTypes as QueryTypes
from .model import CookieTypes as CookieTypes
from .model import FileContent as FileContent
from .model import HTTPVersion as HTTPVersion
from .model import HeaderTypes as HeaderTypes
from .model import SimpleQuery as SimpleQuery
from .model import ContentTypes as ContentTypes
from .driver import ForwardMixin as ForwardMixin
from .model import QueryVariable as QueryVariable
from .driver import ForwardDriver as ForwardDriver
from .driver import ReverseDriver as ReverseDriver
from .driver import combine_driver as combine_driver
from .model import HTTPServerSetup as HTTPServerSetup
from .model import WebSocketServerSetup as WebSocketServerSetup

View File

@ -0,0 +1,234 @@
import abc
import asyncio
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Callable, AsyncGenerator
from nonebot.log import logger
from nonebot.utils import escape_tag
from nonebot.config import Env, Config
from nonebot.dependencies import Dependent
from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook
from nonebot.internal.params import BotParam, DependParam, DefaultParam
from .model import Request, Response, WebSocket, HTTPServerSetup, WebSocketServerSetup
if TYPE_CHECKING:
from nonebot.internal.adapter import Bot, Adapter
BOT_HOOK_PARAMS = [DependParam, BotParam, DefaultParam]
class Driver(abc.ABC):
"""Driver 基类。
参数:
env: 包含环境信息的 Env 对象
config: 包含配置信息的 Config 对象
"""
_adapters: Dict[str, "Adapter"] = {}
"""已注册的适配器列表"""
_bot_connection_hook: Set[Dependent[Any]] = set()
"""Bot 连接建立时执行的函数"""
_bot_disconnection_hook: Set[Dependent[Any]] = set()
"""Bot 连接断开时执行的函数"""
def __init__(self, env: Env, config: Config):
self.env: str = env.environment
"""环境名称"""
self.config: Config = config
"""全局配置对象"""
self._clients: Dict[str, "Bot"] = {}
@property
def bots(self) -> Dict[str, "Bot"]:
"""获取当前所有已连接的 Bot"""
return self._clients
def register_adapter(self, adapter: Type["Adapter"], **kwargs) -> None:
"""注册一个协议适配器
参数:
adapter: 适配器类
kwargs: 其他传递给适配器的参数
"""
name = adapter.get_name()
if name in self._adapters:
logger.opt(colors=True).debug(
f'Adapter "<y>{escape_tag(name)}</y>" already exists'
)
return
self._adapters[name] = adapter(self, **kwargs)
logger.opt(colors=True).debug(
f'Succeeded to load adapter "<y>{escape_tag(name)}</y>"'
)
@property
@abc.abstractmethod
def type(self) -> str:
"""驱动类型名称"""
raise NotImplementedError
@property
@abc.abstractmethod
def logger(self):
"""驱动专属 logger 日志记录器"""
raise NotImplementedError
@abc.abstractmethod
def run(self, *args, **kwargs):
"""
启动驱动框架
"""
logger.opt(colors=True).debug(
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
)
@abc.abstractmethod
def on_startup(self, func: Callable) -> Callable:
"""注册一个在驱动器启动时执行的函数"""
raise NotImplementedError
@abc.abstractmethod
def on_shutdown(self, func: Callable) -> Callable:
"""注册一个在驱动器停止时执行的函数"""
raise NotImplementedError
@classmethod
def on_bot_connect(cls, func: T_BotConnectionHook) -> T_BotConnectionHook:
"""装饰一个函数使他在 bot 连接成功时执行。
钩子函数参数:
- bot: 当前连接上的 Bot 对象
"""
cls._bot_connection_hook.add(
Dependent[Any].parse(call=func, allow_types=BOT_HOOK_PARAMS)
)
return func
@classmethod
def on_bot_disconnect(cls, func: T_BotDisconnectionHook) -> T_BotDisconnectionHook:
"""装饰一个函数使他在 bot 连接断开时执行。
钩子函数参数:
- bot: 当前连接上的 Bot 对象
"""
cls._bot_disconnection_hook.add(
Dependent[Any].parse(call=func, allow_types=BOT_HOOK_PARAMS)
)
return func
def _bot_connect(self, bot: "Bot") -> None:
"""在连接成功后,调用该函数来注册 bot 对象"""
if bot.self_id in self._clients:
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
self._clients[bot.self_id] = bot
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot=bot), self._bot_connection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
asyncio.create_task(_run_hook(bot))
def _bot_disconnect(self, bot: "Bot") -> None:
"""在连接断开后,调用该函数来注销 bot 对象"""
if bot.self_id in self._clients:
del self._clients[bot.self_id]
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot=bot), self._bot_disconnection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
"Running cancelled!</bg #f8bbd0></r>"
)
asyncio.create_task(_run_hook(bot))
class ForwardMixin(abc.ABC):
"""客户端混入基类。"""
@property
@abc.abstractmethod
def type(self) -> str:
"""客户端驱动类型名称"""
raise NotImplementedError
@abc.abstractmethod
async def request(self, setup: Request) -> Response:
"""发送一个 HTTP 请求"""
raise NotImplementedError
@abc.abstractmethod
@asynccontextmanager
async def websocket(self, setup: Request) -> AsyncGenerator[WebSocket, None]:
"""发起一个 WebSocket 连接"""
raise NotImplementedError
yield # used for static type checking's generator detection
class ForwardDriver(Driver, ForwardMixin):
"""客户端基类。将客户端框架封装,以满足适配器使用。"""
class ReverseDriver(Driver):
"""服务端基类。将后端框架封装,以满足适配器使用。"""
@property
@abc.abstractmethod
def server_app(self) -> Any:
"""驱动 APP 对象"""
raise NotImplementedError
@property
@abc.abstractmethod
def asgi(self) -> Any:
"""驱动 ASGI 对象"""
raise NotImplementedError
@abc.abstractmethod
def setup_http_server(self, setup: "HTTPServerSetup") -> None:
"""设置一个 HTTP 服务器路由配置"""
raise NotImplementedError
@abc.abstractmethod
def setup_websocket_server(self, setup: "WebSocketServerSetup") -> None:
"""设置一个 WebSocket 服务器路由配置"""
raise NotImplementedError
def combine_driver(driver: Type[Driver], *mixins: Type[ForwardMixin]) -> Type[Driver]:
"""将一个驱动器和多个混入类合并。"""
# check first
assert issubclass(driver, Driver), "`driver` must be subclass of Driver"
assert all(
map(lambda m: issubclass(m, ForwardMixin), mixins)
), "`mixins` must be subclass of ForwardMixin"
if not mixins:
return driver
class CombinedDriver(*mixins, driver, ForwardDriver): # type: ignore
@property
def type(self) -> str:
return (
driver.type.__get__(self)
+ "+"
+ "+".join(map(lambda x: x.type.__get__(self), mixins))
)
return CombinedDriver

View File

@ -1,5 +1,6 @@
import abc import abc
from enum import Enum from enum import Enum
from dataclasses import dataclass
from http.cookiejar import Cookie, CookieJar from http.cookiejar import Cookie, CookieJar
from typing import ( from typing import (
IO, IO,
@ -9,8 +10,10 @@ from typing import (
Tuple, Tuple,
Union, Union,
Mapping, Mapping,
Callable,
Iterator, Iterator,
Optional, Optional,
Awaitable,
MutableMapping, MutableMapping,
) )
@ -314,3 +317,22 @@ class Cookies(MutableMapping):
) )
return f"<Cookies [{cookies_repr}]>" return f"<Cookies [{cookies_repr}]>"
@dataclass
class HTTPServerSetup:
"""HTTP 服务器路由配置。"""
path: URL # path should not be absolute, check it by URL.is_absolute() == False
method: str
name: str
handle_func: Callable[[Request], Awaitable[Response]]
@dataclass
class WebSocketServerSetup:
"""WebSocket 服务器路由配置。"""
path: URL # path should not be absolute, check it by URL.is_absolute() == False
name: str
handle_func: Callable[[WebSocket], Awaitable[Any]]

721
nonebot/internal/matcher.py Normal file
View File

@ -0,0 +1,721 @@
from types import ModuleType
from datetime import datetime
from contextvars import ContextVar
from collections import defaultdict
from contextlib import AsyncExitStack
from typing import (
TYPE_CHECKING,
Any,
Dict,
List,
Type,
Union,
TypeVar,
Callable,
NoReturn,
Optional,
)
from nonebot.log import logger
from nonebot.dependencies import Dependent
from nonebot.consts import (
ARG_KEY,
RECEIVE_KEY,
REJECT_TARGET,
LAST_RECEIVE_KEY,
REJECT_CACHE_TARGET,
)
from nonebot.typing import (
Any,
T_State,
T_Handler,
T_TypeUpdater,
T_DependencyCache,
T_PermissionUpdater,
)
from nonebot.exception import (
TypeMisMatch,
PausedException,
StopPropagation,
SkippedException,
FinishedException,
RejectedException,
)
from .rule import Rule
from .permission import USER, Permission
from .adapter import Bot, Event, Message, MessageSegment, MessageTemplate
from .params import (
Depends,
ArgParam,
BotParam,
EventParam,
StateParam,
DependParam,
DefaultParam,
MatcherParam,
)
if TYPE_CHECKING:
from nonebot.plugin import Plugin
T = TypeVar("T")
matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list)
"""用于存储当前所有的事件响应器"""
current_bot: ContextVar[Bot] = ContextVar("current_bot")
current_event: ContextVar[Event] = ContextVar("current_event")
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
current_handler: ContextVar[Dependent] = ContextVar("current_handler")
class MatcherMeta(type):
if TYPE_CHECKING:
module: Optional[str]
plugin_name: Optional[str]
module_name: Optional[str]
module_prefix: Optional[str]
type: str
rule: Rule
permission: Permission
handlers: List[T_Handler]
priority: int
block: bool
temp: bool
expire_time: Optional[datetime]
def __repr__(self) -> str:
return (
f"<Matcher from {self.module_name or 'unknown'}, "
f"type={self.type}, priority={self.priority}, "
f"temp={self.temp}>"
)
def __str__(self) -> str:
return repr(self)
class Matcher(metaclass=MatcherMeta):
"""事件响应器类"""
plugin: Optional["Plugin"] = None
"""事件响应器所在插件"""
module: Optional[ModuleType] = None
"""事件响应器所在插件模块"""
plugin_name: Optional[str] = None
"""事件响应器所在插件名"""
module_name: Optional[str] = None
"""事件响应器所在点分割插件模块路径"""
type: str = ""
"""事件响应器类型"""
rule: Rule = Rule()
"""事件响应器匹配规则"""
permission: Permission = Permission()
"""事件响应器触发权限"""
handlers: List[Dependent[Any]] = []
"""事件响应器拥有的事件处理函数列表"""
priority: int = 1
"""事件响应器优先级"""
block: bool = False
"""事件响应器是否阻止事件传播"""
temp: bool = False
"""事件响应器是否为临时"""
expire_time: Optional[datetime] = None
"""事件响应器过期时间点"""
_default_state: T_State = {}
"""事件响应器默认状态"""
_default_type_updater: Optional[Dependent[str]] = None
"""事件响应器类型更新函数"""
_default_permission_updater: Optional[Dependent[Permission]] = None
"""事件响应器权限更新函数"""
HANDLER_PARAM_TYPES = [
DependParam,
BotParam,
EventParam,
StateParam,
ArgParam,
MatcherParam,
DefaultParam,
]
def __init__(self):
self.handlers = self.handlers.copy()
self.state = self._default_state.copy()
def __repr__(self) -> str:
return (
f"<Matcher from {self.module_name or 'unknown'}, type={self.type}, "
f"priority={self.priority}, temp={self.temp}>"
)
def __str__(self) -> str:
return repr(self)
@classmethod
def new(
cls,
type_: str = "",
rule: Optional[Rule] = None,
permission: Optional[Permission] = None,
handlers: Optional[List[Union[T_Handler, Dependent[Any]]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
*,
plugin: Optional["Plugin"] = None,
module: Optional[ModuleType] = None,
expire_time: Optional[datetime] = None,
default_state: Optional[T_State] = None,
default_type_updater: Optional[Union[T_TypeUpdater, Dependent[str]]] = None,
default_permission_updater: Optional[
Union[T_PermissionUpdater, Dependent[Permission]]
] = None,
) -> Type["Matcher"]:
"""
创建一个新的事件响应器并存储至 `matchers <#matchers>`_
参数:
type_: 事件响应器类型 `event.get_type()` 一致时触发空字符串表示任意
rule: 匹配规则
permission: 权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器即触发一次后删除
priority: 响应优先级
block: 是否阻止事件向更低优先级的响应器传播
plugin: 事件响应器所在插件
module: 事件响应器所在模块
default_state: 默认状态 `state`
expire_time: 事件响应器最终有效时间点过时即被删除
返回:
Type[Matcher]: 新的事件响应器类
"""
NewMatcher = type(
"Matcher",
(Matcher,),
{
"plugin": plugin,
"module": module,
"plugin_name": plugin and plugin.name,
"module_name": module and module.__name__,
"type": type_,
"rule": rule or Rule(),
"permission": permission or Permission(),
"handlers": [
handler
if isinstance(handler, Dependent)
else Dependent[Any].parse(
call=handler, allow_types=cls.HANDLER_PARAM_TYPES
)
for handler in handlers
]
if handlers
else [],
"temp": temp,
"expire_time": expire_time,
"priority": priority,
"block": block,
"_default_state": default_state or {},
"_default_type_updater": (
default_type_updater
if isinstance(default_type_updater, Dependent)
else default_type_updater
and Dependent[str].parse(
call=default_type_updater, allow_types=cls.HANDLER_PARAM_TYPES
)
),
"_default_permission_updater": (
default_permission_updater
if isinstance(default_permission_updater, Dependent)
else default_permission_updater
and Dependent[Permission].parse(
call=default_permission_updater,
allow_types=cls.HANDLER_PARAM_TYPES,
)
),
},
)
logger.trace(f"Define new matcher {NewMatcher}")
matchers[priority].append(NewMatcher)
return NewMatcher
@classmethod
async def check_perm(
cls,
bot: Bot,
event: Event,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否满足触发权限
参数:
bot: Bot 对象
event: 上报事件
stack: 异步上下文栈
dependency_cache: 依赖缓存
返回:
是否满足权限
"""
event_type = event.get_type()
return event_type == (cls.type or event_type) and await cls.permission(
bot, event, stack, dependency_cache
)
@classmethod
async def check_rule(
cls,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否满足匹配规则
参数:
bot: Bot 对象
event: 上报事件
state: 当前状态
stack: 异步上下文栈
dependency_cache: 依赖缓存
返回:
是否满足匹配规则
"""
event_type = event.get_type()
return event_type == (cls.type or event_type) and await cls.rule(
bot, event, state, stack, dependency_cache
)
@classmethod
def type_updater(cls, func: T_TypeUpdater) -> T_TypeUpdater:
"""装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数
参数:
func: 响应事件类型更新函数
"""
cls._default_type_updater = Dependent[str].parse(
call=func, allow_types=cls.HANDLER_PARAM_TYPES
)
return func
@classmethod
def permission_updater(cls, func: T_PermissionUpdater) -> T_PermissionUpdater:
"""装饰一个函数来更改当前事件响应器的默认会话权限更新函数
参数:
func: 会话权限更新函数
"""
cls._default_permission_updater = Dependent[Permission].parse(
call=func, allow_types=cls.HANDLER_PARAM_TYPES
)
return func
@classmethod
def append_handler(
cls, handler: T_Handler, parameterless: Optional[List[Any]] = None
) -> Dependent[Any]:
handler_ = Dependent[Any].parse(
call=handler,
parameterless=parameterless,
allow_types=cls.HANDLER_PARAM_TYPES,
)
cls.handlers.append(handler_)
return handler_
@classmethod
def handle(
cls, parameterless: Optional[List[Any]] = None
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来向事件响应器直接添加一个处理函数
参数:
parameterless: 非参数类型依赖列表
"""
def _decorator(func: T_Handler) -> T_Handler:
cls.append_handler(func, parameterless=parameterless)
return func
return _decorator
@classmethod
def receive(
cls, id: str = "", parameterless: Optional[List[Any]] = None
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
参数:
id: 消息 ID
parameterless: 非参数类型依赖列表
"""
async def _receive(event: Event, matcher: "Matcher") -> Union[None, NoReturn]:
matcher.set_target(RECEIVE_KEY.format(id=id))
if matcher.get_target() == RECEIVE_KEY.format(id=id):
matcher.set_receive(id, event)
return
if matcher.get_receive(id, ...) is not ...:
return
await matcher.reject()
_parameterless = [Depends(_receive), *(parameterless or [])]
def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1]
for depend in reversed(_parameterless):
func_handler.prepend_parameterless(depend)
else:
cls.append_handler(func, parameterless=_parameterless)
return func
return _decorator
@classmethod
def got(
cls,
key: str,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
parameterless: Optional[List[Any]] = None,
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 获取一个参数 `key`
当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数如果 `key` 已存在则直接继续运行
参数:
key: 参数名
prompt: 在参数不存在时向用户发送的消息
parameterless: 非参数类型依赖列表
"""
async def _key_getter(event: Event, matcher: "Matcher"):
matcher.set_target(ARG_KEY.format(key=key))
if matcher.get_target() == ARG_KEY.format(key=key):
matcher.set_arg(key, event.get_message())
return
if matcher.get_arg(key, ...) is not ...:
return
await matcher.reject(prompt)
_parameterless = [
Depends(_key_getter),
*(parameterless or []),
]
def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1]
for depend in reversed(_parameterless):
func_handler.prepend_parameterless(depend)
else:
cls.append_handler(func, parameterless=_parameterless)
return func
return _decorator
@classmethod
async def send(
cls,
message: Union[str, Message, MessageSegment, MessageTemplate],
**kwargs: Any,
) -> Any:
"""发送一条消息给当前交互用户
参数:
message: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
bot = current_bot.get()
event = current_event.get()
state = current_matcher.get().state
if isinstance(message, MessageTemplate):
_message = message.format(**state)
else:
_message = message
return await bot.send(event=event, message=_message, **kwargs)
@classmethod
async def finish(
cls,
message: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""发送一条消息给当前交互用户并结束当前事件响应器
参数:
message: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
if message is not None:
await cls.send(message, **kwargs)
raise FinishedException
@classmethod
async def pause(
cls,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数
参数:
prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
if prompt is not None:
await cls.send(prompt, **kwargs)
raise PausedException
@classmethod
async def reject(
cls,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""最近使用 `got` / `receive` 接收的消息不符合预期,
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置在接收用户新的一个事件后从头开始执行当前处理函数
参数:
prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
async def reject_arg(
cls,
key: str,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""最近使用 `got` 接收的消息不符合预期,
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置在接收用户新的一条消息后从头开始执行当前处理函数
参数:
key: 参数名
prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
matcher = current_matcher.get()
matcher.set_target(ARG_KEY.format(key=key))
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
async def reject_receive(
cls,
id: str = "",
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""最近使用 `receive` 接收的消息不符合预期,
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置在接收用户新的一个事件后从头开始执行当前处理函数
参数:
id: 消息 id
prompt: 消息内容
kwargs: {ref}`nonebot.adapters.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
matcher = current_matcher.get()
matcher.set_target(RECEIVE_KEY.format(id=id))
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
def skip(cls) -> NoReturn:
"""跳过当前事件处理函数,继续下一个处理函数
通常在事件处理函数的依赖中使用
"""
raise SkippedException
def get_receive(self, id: str, default: T = None) -> Union[Event, T]:
"""获取一个 `receive` 事件
如果没有找到对应的事件返回 `default`
"""
return self.state.get(RECEIVE_KEY.format(id=id), default)
def set_receive(self, id: str, event: Event) -> None:
"""设置一个 `receive` 事件"""
self.state[RECEIVE_KEY.format(id=id)] = event
self.state[LAST_RECEIVE_KEY] = event
def get_last_receive(self, default: T = None) -> Union[Event, T]:
"""获取最近一次 `receive` 事件
如果没有事件返回 `default`
"""
return self.state.get(LAST_RECEIVE_KEY, default)
def get_arg(self, key: str, default: T = None) -> Union[Message, T]:
"""获取一个 `got` 消息
如果没有找到对应的消息返回 `default`
"""
return self.state.get(ARG_KEY.format(key=key), default)
def set_arg(self, key: str, message: Message) -> None:
"""设置一个 `got` 消息"""
self.state[ARG_KEY.format(key=key)] = message
def set_target(self, target: str, cache: bool = True) -> None:
if cache:
self.state[REJECT_CACHE_TARGET] = target
else:
self.state[REJECT_TARGET] = target
def get_target(self, default: T = None) -> Union[str, T]:
return self.state.get(REJECT_TARGET, default)
def stop_propagation(self):
"""阻止事件传播"""
self.block = True
async def update_type(self, bot: Bot, event: Event) -> str:
updater = self.__class__._default_type_updater
if not updater:
return "message"
return await updater(bot=bot, event=event, state=self.state, matcher=self)
async def update_permission(self, bot: Bot, event: Event) -> Permission:
updater = self.__class__._default_permission_updater
if not updater:
return USER(event.get_session_id(), perm=self.permission)
return await updater(bot=bot, event=event, state=self.state, matcher=self)
async def resolve_reject(self):
handler = current_handler.get()
self.handlers.insert(0, handler)
if REJECT_CACHE_TARGET in self.state:
self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]
async def simple_run(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
):
logger.trace(
f"Matcher {self} run with incoming args: "
f"bot={bot}, event={event}, state={state}"
)
b_t = current_bot.set(bot)
e_t = current_event.set(event)
m_t = current_matcher.set(self)
try:
# Refresh preprocess state
self.state.update(state)
while self.handlers:
handler = self.handlers.pop(0)
current_handler.set(handler)
logger.debug(f"Running handler {handler}")
try:
await handler(
matcher=self,
bot=bot,
event=event,
state=self.state,
stack=stack,
dependency_cache=dependency_cache,
)
except TypeMisMatch as e:
logger.debug(
f"Handler {handler} param {e.param.name} value {e.value} "
f"mismatch type {e.param._type_display()}, skipped"
)
except SkippedException as e:
logger.debug(f"Handler {handler} skipped")
except StopPropagation:
self.block = True
finally:
logger.info(f"Matcher {self} running complete")
current_bot.reset(b_t)
current_event.reset(e_t)
current_matcher.reset(m_t)
# 运行handlers
async def run(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
):
try:
await self.simple_run(bot, event, state, stack, dependency_cache)
except RejectedException:
await self.resolve_reject()
type_ = await self.update_type(bot, event)
permission = await self.update_permission(bot, event)
Matcher.new(
type_,
Rule(),
permission,
self.handlers,
temp=True,
priority=0,
block=True,
plugin=self.plugin,
module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout,
default_state=self.state,
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
)
except PausedException:
type_ = await self.update_type(bot, event)
permission = await self.update_permission(bot, event)
Matcher.new(
type_,
Rule(),
permission,
self.handlers,
temp=True,
priority=0,
block=True,
plugin=self.plugin,
module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout,
default_state=self.state,
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
)
except FinishedException:
pass
__autodoc__ = {
"MatcherMeta": False,
"Matcher.get_target": False,
"Matcher.set_target": False,
"Matcher.update_type": False,
"Matcher.update_permission": False,
"Matcher.resolve_reject": False,
"Matcher.simple_run": False,
}

378
nonebot/internal/params.py Normal file
View File

@ -0,0 +1,378 @@
import asyncio
import inspect
import warnings
from typing_extensions import Literal
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
from pydantic.fields import Required, Undefined, ModelField
from nonebot.log import logger
from nonebot.exception import TypeMisMatch
from nonebot.dependencies.utils import check_field_type
from nonebot.dependencies import Param, Dependent, CustomConfig
from nonebot.typing import T_State, T_Handler, T_DependencyCache
from nonebot.utils import (
get_name,
run_sync,
is_gen_callable,
run_sync_ctx_manager,
is_async_gen_callable,
is_coroutine_callable,
generic_check_issubclass,
)
if TYPE_CHECKING:
from nonebot.matcher import Matcher
from nonebot.adapters import Bot, Event
class DependsInner:
def __init__(
self,
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
) -> None:
self.dependency = dependency
self.use_cache = use_cache
def __repr__(self) -> str:
dep = get_name(self.dependency)
cache = "" if self.use_cache else ", use_cache=False"
return f"{self.__class__.__name__}({dep}{cache})"
def Depends(
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
) -> Any:
"""子依赖装饰器
参数:
dependency: 依赖函数默认为参数的类型注释
use_cache: 是否使用缓存默认为 `True`
用法:
```python
def depend_func() -> Any:
return ...
def depend_gen_func():
try:
yield ...
finally:
...
async def handler(param_name: Any = Depends(depend_func), gen: Any = Depends(depend_gen_func)):
...
```
"""
return DependsInner(dependency, use_cache=use_cache)
class DependParam(Param):
"""子依赖参数"""
@classmethod
def _check_param(
cls,
dependent: Dependent,
name: str,
param: inspect.Parameter,
) -> Optional["DependParam"]:
if isinstance(param.default, DependsInner):
dependency: T_Handler
if param.default.dependency is None:
assert param.annotation is not param.empty, "Dependency cannot be empty"
dependency = param.annotation
else:
dependency = param.default.dependency
sub_dependent = Dependent[Any].parse(
call=dependency,
allow_types=dependent.allow_types,
)
dependent.pre_checkers.extend(sub_dependent.pre_checkers)
sub_dependent.pre_checkers.clear()
return cls(
Required, use_cache=param.default.use_cache, dependent=sub_dependent
)
@classmethod
def _check_parameterless(
cls, dependent: "Dependent", value: Any
) -> Optional["Param"]:
if isinstance(value, DependsInner):
assert value.dependency, "Dependency cannot be empty"
dependent = Dependent[Any].parse(
call=value.dependency, allow_types=dependent.allow_types
)
return cls(Required, use_cache=value.use_cache, dependent=dependent)
async def _solve(
self,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
**kwargs: Any,
) -> Any:
use_cache: bool = self.extra["use_cache"]
dependency_cache = {} if dependency_cache is None else dependency_cache
sub_dependent: Dependent = self.extra["dependent"]
sub_dependent.call = cast(Callable[..., Any], sub_dependent.call)
call = sub_dependent.call
# solve sub dependency with current cache
sub_values = await sub_dependent.solve(
stack=stack,
dependency_cache=dependency_cache,
**kwargs,
)
# run dependency function
task: asyncio.Task[Any]
if use_cache and call in dependency_cache:
solved = await dependency_cache[call]
elif is_gen_callable(call) or is_async_gen_callable(call):
assert isinstance(
stack, AsyncExitStack
), "Generator dependency should be called in context"
if is_gen_callable(call):
cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))
else:
cm = asynccontextmanager(call)(**sub_values)
task = asyncio.create_task(stack.enter_async_context(cm))
dependency_cache[call] = task
solved = await task
elif is_coroutine_callable(call):
task = asyncio.create_task(call(**sub_values))
dependency_cache[call] = task
solved = await task
else:
task = asyncio.create_task(run_sync(call)(**sub_values))
dependency_cache[call] = task
solved = await task
return solved
class _BotChecker(Param):
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
field: ModelField = self.extra["field"]
try:
return check_field_type(field, bot)
except TypeMisMatch:
logger.debug(
f"Bot type {type(bot)} not match "
f"annotation {field._type_display()}, ignored"
)
raise
class BotParam(Param):
"""{ref}`nonebot.adapters.Bot` 参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["BotParam"]:
from nonebot.adapters import Bot
if param.default == param.empty:
if generic_check_issubclass(param.annotation, Bot):
if param.annotation is not Bot:
dependent.pre_checkers.append(
_BotChecker(
Required,
field=ModelField(
name=name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
),
)
)
return cls(Required)
elif param.annotation == param.empty and name == "bot":
return cls(Required)
async def _solve(self, bot: "Bot", **kwargs: Any) -> Any:
return bot
class _EventChecker(Param):
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
field: ModelField = self.extra["field"]
try:
return check_field_type(field, event)
except TypeMisMatch:
logger.debug(
f"Event type {type(event)} not match "
f"annotation {field._type_display()}, ignored"
)
raise
class EventParam(Param):
"""{ref}`nonebot.adapters.Event` 参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["EventParam"]:
from nonebot.adapters import Event
if param.default == param.empty:
if generic_check_issubclass(param.annotation, Event):
if param.annotation is not Event:
dependent.pre_checkers.append(
_EventChecker(
Required,
field=ModelField(
name=name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
),
)
)
return cls(Required)
elif param.annotation == param.empty and name == "event":
return cls(Required)
async def _solve(self, event: "Event", **kwargs: Any) -> Any:
return event
class StateInner(T_State):
...
def State() -> T_State:
"""**Deprecated**: 事件处理状态参数,请直接使用 {ref}`nonebot.typing.T_State`"""
warnings.warn("State() is deprecated, use `T_State` instead", DeprecationWarning)
return StateInner()
class StateParam(Param):
"""事件处理状态参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["StateParam"]:
if isinstance(param.default, StateInner):
return cls(Required)
elif param.default == param.empty:
if param.annotation is T_State:
return cls(Required)
elif param.annotation == param.empty and name == "state":
return cls(Required)
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
return state
class MatcherParam(Param):
"""事件响应器实例参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["MatcherParam"]:
from nonebot.matcher import Matcher
if generic_check_issubclass(param.annotation, Matcher) or (
param.annotation == param.empty and name == "matcher"
):
return cls(Required)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
return matcher
class ArgInner:
def __init__(
self, key: Optional[str], type: Literal["message", "str", "plaintext"]
) -> None:
self.key = key
self.type = type
def Arg(key: Optional[str] = None) -> Any:
"""`got` 的 Arg 参数消息"""
return ArgInner(key, "message")
def ArgStr(key: Optional[str] = None) -> str:
"""`got` 的 Arg 参数消息文本"""
return ArgInner(key, "str") # type: ignore
def ArgPlainText(key: Optional[str] = None) -> str:
"""`got` 的 Arg 参数消息纯文本"""
return ArgInner(key, "plaintext") # type: ignore
class ArgParam(Param):
"""`got` 的 Arg 参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["ArgParam"]:
if isinstance(param.default, ArgInner):
return cls(Required, key=param.default.key or name, type=param.default.type)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
message = matcher.get_arg(self.extra["key"])
if message is None:
return message
if self.extra["type"] == "message":
return message
elif self.extra["type"] == "str":
return str(message)
else:
return message.extract_plain_text()
class ExceptionParam(Param):
"""`run_postprocessor` 的异常参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["ExceptionParam"]:
if generic_check_issubclass(param.annotation, Exception) or (
param.annotation == param.empty and name == "exception"
):
return cls(Required)
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
return exception
class DefaultParam(Param):
"""默认值参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["DefaultParam"]:
if param.default != param.empty:
return cls(param.default)
async def _solve(self, **kwargs: Any) -> Any:
return Undefined
__autodoc__ = {
"DependsInner": False,
"StateInner": False,
"ArgInner": False,
}

View File

@ -0,0 +1,133 @@
import asyncio
from contextlib import AsyncExitStack
from typing import Any, Set, Tuple, Union, NoReturn, Optional, Coroutine
from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException
from nonebot.typing import T_DependencyCache, T_PermissionChecker
from .adapter import Bot, Event
from .params import BotParam, EventParam, DependParam, DefaultParam
async def _run_coro_with_catch(coro: Coroutine[Any, Any, Any]):
try:
return await coro
except SkippedException:
return False
class Permission:
"""{ref}`nonebot.matcher.Matcher` 权限类。
当事件传递时 {ref}`nonebot.matcher.Matcher` 运行前进行检查
参数:
checkers: PermissionChecker
用法:
```python
Permission(async_function) | sync_function
# 等价于
Permission(async_function, sync_function)
```
"""
__slots__ = ("checkers",)
HANDLER_PARAM_TYPES = [
DependParam,
BotParam,
EventParam,
DefaultParam,
]
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = set(
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
for checker in checkers
)
"""存储 `PermissionChecker`"""
async def __call__(
self,
bot: Bot,
event: Event,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否满足某个权限
参数:
bot: Bot 对象
event: Event 对象
stack: 异步上下文栈
dependency_cache: 依赖缓存
"""
if not self.checkers:
return True
results = await asyncio.gather(
*(
_run_coro_with_catch(
checker(
bot=bot,
event=event,
stack=stack,
dependency_cache=dependency_cache,
)
)
for checker in self.checkers
),
)
return any(results)
def __and__(self, other) -> NoReturn:
raise RuntimeError("And operation between Permissions is not allowed.")
def __or__(
self, other: Optional[Union["Permission", T_PermissionChecker]]
) -> "Permission":
if other is None:
return self
elif isinstance(other, Permission):
return Permission(*self.checkers, *other.checkers)
else:
return Permission(*self.checkers, other)
class User:
"""检查当前事件是否属于指定会话
参数:
users: 会话 ID 元组
perm: 需同时满足的权限
"""
__slots__ = ("users", "perm")
def __init__(
self, users: Tuple[str, ...], perm: Optional[Permission] = None
) -> None:
self.users = users
self.perm = perm
async def __call__(self, bot: Bot, event: Event) -> bool:
return bool(
event.get_session_id() in self.users
and (self.perm is None or await self.perm(bot, event))
)
def USER(*users: str, perm: Optional[Permission] = None):
"""匹配当前事件属于指定会话
参数:
user: 会话白名单
perm: 需要同时满足的权限
"""
return Permission(User(users, perm))

95
nonebot/internal/rule.py Normal file
View File

@ -0,0 +1,95 @@
import asyncio
from contextlib import AsyncExitStack
from typing import Set, Union, NoReturn, Optional
from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException
from nonebot.typing import T_State, T_RuleChecker, T_DependencyCache
from .adapter import Bot, Event
from .params import BotParam, EventParam, StateParam, DependParam, DefaultParam
class Rule:
"""{ref}`nonebot.matcher.Matcher` 规则类。
当事件传递时 {ref}`nonebot.matcher.Matcher` 运行前进行检查
参数:
*checkers: RuleChecker
用法:
```python
Rule(async_function) & sync_function
# 等价于
Rule(async_function, sync_function)
```
"""
__slots__ = ("checkers",)
HANDLER_PARAM_TYPES = [
DependParam,
BotParam,
EventParam,
StateParam,
DefaultParam,
]
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = set(
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
for checker in checkers
)
"""存储 `RuleChecker`"""
async def __call__(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否符合所有规则
参数:
bot: Bot 对象
event: Event 对象
state: 当前 State
stack: 异步上下文栈
dependency_cache: 依赖缓存
"""
if not self.checkers:
return True
try:
results = await asyncio.gather(
*(
checker(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
)
for checker in self.checkers
)
)
except SkippedException:
return False
return all(results)
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
if other is None:
return self
elif isinstance(other, Rule):
return Rule(*self.checkers, *other.checkers)
else:
return Rule(*self.checkers, other)
def __or__(self, other) -> NoReturn:
raise RuntimeError("Or operation between rules is not allowed.")

View File

@ -5,697 +5,14 @@ FrontMatter:
description: nonebot.matcher 模块 description: nonebot.matcher 模块
""" """
from types import ModuleType from nonebot.internal.matcher import Matcher as Matcher
from datetime import datetime from nonebot.internal.matcher import matchers as matchers
from contextvars import ContextVar from nonebot.internal.matcher import current_bot as current_bot
from collections import defaultdict from nonebot.internal.matcher import current_event as current_event
from contextlib import AsyncExitStack from nonebot.internal.matcher import current_handler as current_handler
from typing import ( from nonebot.internal.matcher import current_matcher as current_matcher
TYPE_CHECKING,
Any,
Dict,
List,
Type,
Union,
TypeVar,
Callable,
NoReturn,
Optional,
)
from nonebot import params
from nonebot.rule import Rule
from nonebot.log import logger
from nonebot.dependencies import Dependent
from nonebot.permission import USER, Permission
from nonebot.adapters import Bot, Event, Message, MessageSegment, MessageTemplate
from nonebot.consts import (
ARG_KEY,
RECEIVE_KEY,
REJECT_TARGET,
LAST_RECEIVE_KEY,
REJECT_CACHE_TARGET,
)
from nonebot.typing import (
Any,
T_State,
T_Handler,
T_TypeUpdater,
T_DependencyCache,
T_PermissionUpdater,
)
from nonebot.exception import (
TypeMisMatch,
PausedException,
StopPropagation,
SkippedException,
FinishedException,
RejectedException,
)
if TYPE_CHECKING:
from nonebot.plugin import Plugin
T = TypeVar("T")
matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list)
"""用于存储当前所有的事件响应器"""
current_bot: ContextVar[Bot] = ContextVar("current_bot")
current_event: ContextVar[Event] = ContextVar("current_event")
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
current_handler: ContextVar[Dependent] = ContextVar("current_handler")
class MatcherMeta(type):
if TYPE_CHECKING:
module: Optional[str]
plugin_name: Optional[str]
module_name: Optional[str]
module_prefix: Optional[str]
type: str
rule: Rule
permission: Permission
handlers: List[T_Handler]
priority: int
block: bool
temp: bool
expire_time: Optional[datetime]
def __repr__(self) -> str:
return (
f"<Matcher from {self.module_name or 'unknown'}, "
f"type={self.type}, priority={self.priority}, "
f"temp={self.temp}>"
)
def __str__(self) -> str:
return repr(self)
class Matcher(metaclass=MatcherMeta):
"""事件响应器类"""
plugin: Optional["Plugin"] = None
"""事件响应器所在插件"""
module: Optional[ModuleType] = None
"""事件响应器所在插件模块"""
plugin_name: Optional[str] = None
"""事件响应器所在插件名"""
module_name: Optional[str] = None
"""事件响应器所在点分割插件模块路径"""
type: str = ""
"""事件响应器类型"""
rule: Rule = Rule()
"""事件响应器匹配规则"""
permission: Permission = Permission()
"""事件响应器触发权限"""
handlers: List[Dependent[Any]] = []
"""事件响应器拥有的事件处理函数列表"""
priority: int = 1
"""事件响应器优先级"""
block: bool = False
"""事件响应器是否阻止事件传播"""
temp: bool = False
"""事件响应器是否为临时"""
expire_time: Optional[datetime] = None
"""事件响应器过期时间点"""
_default_state: T_State = {}
"""事件响应器默认状态"""
_default_type_updater: Optional[Dependent[str]] = None
"""事件响应器类型更新函数"""
_default_permission_updater: Optional[Dependent[Permission]] = None
"""事件响应器权限更新函数"""
HANDLER_PARAM_TYPES = [
params.DependParam,
params.BotParam,
params.EventParam,
params.StateParam,
params.ArgParam,
params.MatcherParam,
params.DefaultParam,
]
def __init__(self):
self.handlers = self.handlers.copy()
self.state = self._default_state.copy()
def __repr__(self) -> str:
return (
f"<Matcher from {self.module_name or 'unknown'}, type={self.type}, "
f"priority={self.priority}, temp={self.temp}>"
)
def __str__(self) -> str:
return repr(self)
@classmethod
def new(
cls,
type_: str = "",
rule: Optional[Rule] = None,
permission: Optional[Permission] = None,
handlers: Optional[List[Union[T_Handler, Dependent[Any]]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
*,
plugin: Optional["Plugin"] = None,
module: Optional[ModuleType] = None,
expire_time: Optional[datetime] = None,
default_state: Optional[T_State] = None,
default_type_updater: Optional[T_TypeUpdater] = None,
default_permission_updater: Optional[T_PermissionUpdater] = None,
) -> Type["Matcher"]:
"""
创建一个新的事件响应器并存储至 `matchers <#matchers>`_
参数:
type_: 事件响应器类型 `event.get_type()` 一致时触发空字符串表示任意
rule: 匹配规则
permission: 权限
handlers: 事件处理函数列表
temp: 是否为临时事件响应器即触发一次后删除
priority: 响应优先级
block: 是否阻止事件向更低优先级的响应器传播
plugin: 事件响应器所在插件
module: 事件响应器所在模块
default_state: 默认状态 `state`
expire_time: 事件响应器最终有效时间点过时即被删除
返回:
Type[Matcher]: 新的事件响应器类
"""
NewMatcher = type(
"Matcher",
(Matcher,),
{
"plugin": plugin,
"module": module,
"plugin_name": plugin and plugin.name,
"module_name": module and module.__name__,
"type": type_,
"rule": rule or Rule(),
"permission": permission or Permission(),
"handlers": [
handler
if isinstance(handler, Dependent)
else Dependent[Any].parse(
call=handler, allow_types=cls.HANDLER_PARAM_TYPES
)
for handler in handlers
]
if handlers
else [],
"temp": temp,
"expire_time": expire_time,
"priority": priority,
"block": block,
"_default_state": default_state or {},
"_default_type_updater": default_type_updater,
"_default_permission_updater": default_permission_updater,
},
)
logger.trace(f"Define new matcher {NewMatcher}")
matchers[priority].append(NewMatcher)
return NewMatcher
@classmethod
async def check_perm(
cls,
bot: Bot,
event: Event,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否满足触发权限
参数:
bot: Bot 对象
event: 上报事件
stack: 异步上下文栈
dependency_cache: 依赖缓存
返回:
是否满足权限
"""
event_type = event.get_type()
return event_type == (cls.type or event_type) and await cls.permission(
bot, event, stack, dependency_cache
)
@classmethod
async def check_rule(
cls,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否满足匹配规则
参数:
bot: Bot 对象
event: 上报事件
state: 当前状态
stack: 异步上下文栈
dependency_cache: 依赖缓存
返回:
是否满足匹配规则
"""
event_type = event.get_type()
return event_type == (cls.type or event_type) and await cls.rule(
bot, event, state, stack, dependency_cache
)
@classmethod
def type_updater(cls, func: T_TypeUpdater) -> T_TypeUpdater:
"""装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数
参数:
func: 响应事件类型更新函数
"""
cls._default_type_updater = Dependent[str].parse(
call=func, allow_types=cls.HANDLER_PARAM_TYPES
)
return func
@classmethod
def permission_updater(cls, func: T_PermissionUpdater) -> T_PermissionUpdater:
"""装饰一个函数来更改当前事件响应器的默认会话权限更新函数
参数:
func: 会话权限更新函数
"""
cls._default_permission_updater = Dependent[Permission].parse(
call=func, allow_types=cls.HANDLER_PARAM_TYPES
)
return func
@classmethod
def append_handler(
cls, handler: T_Handler, parameterless: Optional[List[Any]] = None
) -> Dependent[Any]:
handler_ = Dependent[Any].parse(
call=handler,
parameterless=parameterless,
allow_types=cls.HANDLER_PARAM_TYPES,
)
cls.handlers.append(handler_)
return handler_
@classmethod
def handle(
cls, parameterless: Optional[List[Any]] = None
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来向事件响应器直接添加一个处理函数
参数:
parameterless: 非参数类型依赖列表
"""
def _decorator(func: T_Handler) -> T_Handler:
cls.append_handler(func, parameterless=parameterless)
return func
return _decorator
@classmethod
def receive(
cls, id: str = "", parameterless: Optional[List[Any]] = None
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数
参数:
id: 消息 ID
parameterless: 非参数类型依赖列表
"""
async def _receive(event: Event, matcher: "Matcher") -> Union[None, NoReturn]:
matcher.set_target(RECEIVE_KEY.format(id=id))
if matcher.get_target() == RECEIVE_KEY.format(id=id):
matcher.set_receive(id, event)
return
if matcher.get_receive(id, ...) is not ...:
return
await matcher.reject()
_parameterless = [params.Depends(_receive), *(parameterless or [])]
def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1]
for depend in reversed(_parameterless):
func_handler.prepend_parameterless(depend)
else:
cls.append_handler(func, parameterless=_parameterless)
return func
return _decorator
@classmethod
def got(
cls,
key: str,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
parameterless: Optional[List[Any]] = None,
) -> Callable[[T_Handler], T_Handler]:
"""装饰一个函数来指示 NoneBot 获取一个参数 `key`
当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数如果 `key` 已存在则直接继续运行
参数:
key: 参数名
prompt: 在参数不存在时向用户发送的消息
parameterless: 非参数类型依赖列表
"""
async def _key_getter(event: Event, matcher: "Matcher"):
matcher.set_target(ARG_KEY.format(key=key))
if matcher.get_target() == ARG_KEY.format(key=key):
matcher.set_arg(key, event.get_message())
return
if matcher.get_arg(key, ...) is not ...:
return
await matcher.reject(prompt)
_parameterless = [
params.Depends(_key_getter),
*(parameterless or []),
]
def _decorator(func: T_Handler) -> T_Handler:
if cls.handlers and cls.handlers[-1].call is func:
func_handler = cls.handlers[-1]
for depend in reversed(_parameterless):
func_handler.prepend_parameterless(depend)
else:
cls.append_handler(func, parameterless=_parameterless)
return func
return _decorator
@classmethod
async def send(
cls,
message: Union[str, Message, MessageSegment, MessageTemplate],
**kwargs: Any,
) -> Any:
"""发送一条消息给当前交互用户
参数:
message: 消息内容
kwargs: {ref}`nonebot.adapters._bot.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
bot = current_bot.get()
event = current_event.get()
state = current_matcher.get().state
if isinstance(message, MessageTemplate):
_message = message.format(**state)
else:
_message = message
return await bot.send(event=event, message=_message, **kwargs)
@classmethod
async def finish(
cls,
message: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""发送一条消息给当前交互用户并结束当前事件响应器
参数:
message: 消息内容
kwargs: {ref}`nonebot.adapters._bot.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
if message is not None:
await cls.send(message, **kwargs)
raise FinishedException
@classmethod
async def pause(
cls,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数
参数:
prompt: 消息内容
kwargs: {ref}`nonebot.adapters._bot.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
if prompt is not None:
await cls.send(prompt, **kwargs)
raise PausedException
@classmethod
async def reject(
cls,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""最近使用 `got` / `receive` 接收的消息不符合预期,
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置在接收用户新的一个事件后从头开始执行当前处理函数
参数:
prompt: 消息内容
kwargs: {ref}`nonebot.adapters._bot.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
async def reject_arg(
cls,
key: str,
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""最近使用 `got` 接收的消息不符合预期,
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置在接收用户新的一条消息后从头开始执行当前处理函数
参数:
key: 参数名
prompt: 消息内容
kwargs: {ref}`nonebot.adapters._bot.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
matcher = current_matcher.get()
matcher.set_target(ARG_KEY.format(key=key))
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
async def reject_receive(
cls,
id: str = "",
prompt: Optional[Union[str, Message, MessageSegment, MessageTemplate]] = None,
**kwargs,
) -> NoReturn:
"""最近使用 `receive` 接收的消息不符合预期,
发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置在接收用户新的一个事件后从头开始执行当前处理函数
参数:
id: 消息 id
prompt: 消息内容
kwargs: {ref}`nonebot.adapters._bot.Bot.send` 的参数请参考对应 adapter bot 对象 api
"""
matcher = current_matcher.get()
matcher.set_target(RECEIVE_KEY.format(id=id))
if prompt is not None:
await cls.send(prompt, **kwargs)
raise RejectedException
@classmethod
def skip(cls) -> NoReturn:
"""跳过当前事件处理函数,继续下一个处理函数
通常在事件处理函数的依赖中使用
"""
raise SkippedException
def get_receive(self, id: str, default: T = None) -> Union[Event, T]:
"""获取一个 `receive` 事件
如果没有找到对应的事件返回 `default`
"""
return self.state.get(RECEIVE_KEY.format(id=id), default)
def set_receive(self, id: str, event: Event) -> None:
"""设置一个 `receive` 事件"""
self.state[RECEIVE_KEY.format(id=id)] = event
self.state[LAST_RECEIVE_KEY] = event
def get_last_receive(self, default: T = None) -> Union[Event, T]:
"""获取最近一次 `receive` 事件
如果没有事件返回 `default`
"""
return self.state.get(LAST_RECEIVE_KEY, default)
def get_arg(self, key: str, default: T = None) -> Union[Message, T]:
"""获取一个 `got` 消息
如果没有找到对应的消息返回 `default`
"""
return self.state.get(ARG_KEY.format(key=key), default)
def set_arg(self, key: str, message: Message) -> None:
"""设置一个 `got` 消息"""
self.state[ARG_KEY.format(key=key)] = message
def set_target(self, target: str, cache: bool = True) -> None:
if cache:
self.state[REJECT_CACHE_TARGET] = target
else:
self.state[REJECT_TARGET] = target
def get_target(self, default: T = None) -> Union[str, T]:
return self.state.get(REJECT_TARGET, default)
def stop_propagation(self):
"""阻止事件传播"""
self.block = True
async def update_type(self, bot: Bot, event: Event) -> str:
updater = self.__class__._default_type_updater
if not updater:
return "message"
return await updater(bot=bot, event=event, state=self.state, matcher=self)
async def update_permission(self, bot: Bot, event: Event) -> Permission:
updater = self.__class__._default_permission_updater
if not updater:
return USER(event.get_session_id(), perm=self.permission)
return await updater(bot=bot, event=event, state=self.state, matcher=self)
async def resolve_reject(self):
handler = current_handler.get()
self.handlers.insert(0, handler)
if REJECT_CACHE_TARGET in self.state:
self.state[REJECT_TARGET] = self.state[REJECT_CACHE_TARGET]
async def simple_run(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
):
logger.trace(
f"Matcher {self} run with incoming args: "
f"bot={bot}, event={event}, state={state}"
)
b_t = current_bot.set(bot)
e_t = current_event.set(event)
m_t = current_matcher.set(self)
try:
# Refresh preprocess state
self.state.update(state)
while self.handlers:
handler = self.handlers.pop(0)
current_handler.set(handler)
logger.debug(f"Running handler {handler}")
try:
await handler(
matcher=self,
bot=bot,
event=event,
state=self.state,
stack=stack,
dependency_cache=dependency_cache,
)
except TypeMisMatch as e:
logger.debug(
f"Handler {handler} param {e.param.name} value {e.value} "
f"mismatch type {e.param._type_display()}, skipped"
)
except SkippedException as e:
logger.debug(f"Handler {handler} skipped")
except StopPropagation:
self.block = True
finally:
logger.info(f"Matcher {self} running complete")
current_bot.reset(b_t)
current_event.reset(e_t)
current_matcher.reset(m_t)
# 运行handlers
async def run(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
):
try:
await self.simple_run(bot, event, state, stack, dependency_cache)
except RejectedException:
await self.resolve_reject()
type_ = await self.update_type(bot, event)
permission = await self.update_permission(bot, event)
Matcher.new(
type_,
Rule(),
permission,
self.handlers,
temp=True,
priority=0,
block=True,
plugin=self.plugin,
module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout,
default_state=self.state,
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
)
except PausedException:
type_ = await self.update_type(bot, event)
permission = await self.update_permission(bot, event)
Matcher.new(
type_,
Rule(),
permission,
self.handlers,
temp=True,
priority=0,
block=True,
plugin=self.plugin,
module=self.module,
expire_time=datetime.now() + bot.config.session_expire_timeout,
default_state=self.state,
default_type_updater=self.__class__._default_type_updater,
default_permission_updater=self.__class__._default_permission_updater,
)
except FinishedException:
pass
__autodoc__ = { __autodoc__ = {
"MatcherMeta": False, "Matcher": True,
"Matcher.get_target": False, "matchers": True,
"Matcher.set_target": False,
"Matcher.update_type": False,
"Matcher.update_permission": False,
"Matcher.resolve_reject": False,
"Matcher.simple_run": False,
} }

View File

@ -12,7 +12,6 @@ from datetime import datetime
from contextlib import AsyncExitStack from contextlib import AsyncExitStack
from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional, Coroutine from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional, Coroutine
from nonebot import params
from nonebot.log import logger from nonebot.log import logger
from nonebot.rule import TrieRule from nonebot.rule import TrieRule
from nonebot.utils import escape_tag from nonebot.utils import escape_tag
@ -32,6 +31,16 @@ from nonebot.typing import (
T_EventPreProcessor, T_EventPreProcessor,
T_EventPostProcessor, T_EventPostProcessor,
) )
from nonebot.internal.params import (
ArgParam,
BotParam,
EventParam,
StateParam,
DependParam,
DefaultParam,
MatcherParam,
ExceptionParam,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
@ -42,30 +51,30 @@ _run_preprocessors: Set[Dependent[Any]] = set()
_run_postprocessors: Set[Dependent[Any]] = set() _run_postprocessors: Set[Dependent[Any]] = set()
EVENT_PCS_PARAMS = [ EVENT_PCS_PARAMS = [
params.DependParam, DependParam,
params.BotParam, BotParam,
params.EventParam, EventParam,
params.StateParam, StateParam,
params.DefaultParam, DefaultParam,
] ]
RUN_PREPCS_PARAMS = [ RUN_PREPCS_PARAMS = [
params.DependParam, DependParam,
params.BotParam, BotParam,
params.EventParam, EventParam,
params.StateParam, StateParam,
params.ArgParam, ArgParam,
params.MatcherParam, MatcherParam,
params.DefaultParam, DefaultParam,
] ]
RUN_POSTPCS_PARAMS = [ RUN_POSTPCS_PARAMS = [
params.DependParam, DependParam,
params.ExceptionParam, ExceptionParam,
params.BotParam, BotParam,
params.EventParam, EventParam,
params.StateParam, StateParam,
params.ArgParam, ArgParam,
params.MatcherParam, MatcherParam,
params.DefaultParam, DefaultParam,
] ]

View File

@ -5,21 +5,24 @@ FrontMatter:
description: nonebot.params 模块 description: nonebot.params 模块
""" """
import asyncio from typing import Any, Dict, List, Tuple, Optional
import inspect
import warnings
from typing_extensions import Literal
from typing import Any, Dict, List, Tuple, Callable, Optional, cast
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
from pydantic.fields import Required, Undefined, ModelField from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.log import logger from nonebot.adapters import Event, Message
from nonebot.exception import TypeMisMatch from nonebot.internal.params import Arg as Arg
from nonebot.adapters import Bot, Event, Message from nonebot.internal.params import State as State
from nonebot.dependencies.utils import check_field_type from nonebot.internal.params import ArgStr as ArgStr
from nonebot.dependencies import Param, Dependent, CustomConfig from nonebot.internal.params import Depends as Depends
from nonebot.typing import T_State, T_Handler, T_DependencyCache from nonebot.internal.params import ArgParam as ArgParam
from nonebot.internal.params import BotParam as BotParam
from nonebot.internal.params import EventParam as EventParam
from nonebot.internal.params import StateParam as StateParam
from nonebot.internal.params import DependParam as DependParam
from nonebot.internal.params import ArgPlainText as ArgPlainText
from nonebot.internal.params import DefaultParam as DefaultParam
from nonebot.internal.params import MatcherParam as MatcherParam
from nonebot.internal.params import ExceptionParam as ExceptionParam
from nonebot.consts import ( from nonebot.consts import (
CMD_KEY, CMD_KEY,
PREFIX_KEY, PREFIX_KEY,
@ -31,233 +34,6 @@ from nonebot.consts import (
REGEX_GROUP, REGEX_GROUP,
REGEX_MATCHED, REGEX_MATCHED,
) )
from nonebot.utils import (
get_name,
run_sync,
is_gen_callable,
run_sync_ctx_manager,
is_async_gen_callable,
is_coroutine_callable,
generic_check_issubclass,
)
class DependsInner:
def __init__(
self,
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
) -> None:
self.dependency = dependency
self.use_cache = use_cache
def __repr__(self) -> str:
dep = get_name(self.dependency)
cache = "" if self.use_cache else ", use_cache=False"
return f"{self.__class__.__name__}({dep}{cache})"
def Depends(
dependency: Optional[T_Handler] = None,
*,
use_cache: bool = True,
) -> Any:
"""子依赖装饰器
参数:
dependency: 依赖函数默认为参数的类型注释
use_cache: 是否使用缓存默认为 `True`
用法:
```python
def depend_func() -> Any:
return ...
def depend_gen_func():
try:
yield ...
finally:
...
async def handler(param_name: Any = Depends(depend_func), gen: Any = Depends(depend_gen_func)):
...
```
"""
return DependsInner(dependency, use_cache=use_cache)
class DependParam(Param):
"""子依赖参数"""
@classmethod
def _check_param(
cls,
dependent: Dependent,
name: str,
param: inspect.Parameter,
) -> Optional["DependParam"]:
if isinstance(param.default, DependsInner):
dependency: T_Handler
if param.default.dependency is None:
assert param.annotation is not param.empty, "Dependency cannot be empty"
dependency = param.annotation
else:
dependency = param.default.dependency
sub_dependent = Dependent[Any].parse(
call=dependency,
allow_types=dependent.allow_types,
)
dependent.pre_checkers.extend(sub_dependent.pre_checkers)
sub_dependent.pre_checkers.clear()
return cls(
Required, use_cache=param.default.use_cache, dependent=sub_dependent
)
@classmethod
def _check_parameterless(
cls, dependent: "Dependent", value: Any
) -> Optional["Param"]:
if isinstance(value, DependsInner):
assert value.dependency, "Dependency cannot be empty"
dependent = Dependent[Any].parse(
call=value.dependency, allow_types=dependent.allow_types
)
return cls(Required, use_cache=value.use_cache, dependent=dependent)
async def _solve(
self,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
**kwargs: Any,
) -> Any:
use_cache: bool = self.extra["use_cache"]
dependency_cache = {} if dependency_cache is None else dependency_cache
sub_dependent: Dependent = self.extra["dependent"]
sub_dependent.call = cast(Callable[..., Any], sub_dependent.call)
call = sub_dependent.call
# solve sub dependency with current cache
sub_values = await sub_dependent.solve(
stack=stack,
dependency_cache=dependency_cache,
**kwargs,
)
# run dependency function
task: asyncio.Task[Any]
if use_cache and call in dependency_cache:
solved = await dependency_cache[call]
elif is_gen_callable(call) or is_async_gen_callable(call):
assert isinstance(
stack, AsyncExitStack
), "Generator dependency should be called in context"
if is_gen_callable(call):
cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))
else:
cm = asynccontextmanager(call)(**sub_values)
task = asyncio.create_task(stack.enter_async_context(cm))
dependency_cache[call] = task
solved = await task
elif is_coroutine_callable(call):
task = asyncio.create_task(call(**sub_values))
dependency_cache[call] = task
solved = await task
else:
task = asyncio.create_task(run_sync(call)(**sub_values))
dependency_cache[call] = task
solved = await task
return solved
class _BotChecker(Param):
async def _solve(self, bot: Bot, **kwargs: Any) -> Any:
field: ModelField = self.extra["field"]
try:
return check_field_type(field, bot)
except TypeMisMatch:
logger.debug(
f"Bot type {type(bot)} not match "
f"annotation {field._type_display()}, ignored"
)
raise
class BotParam(Param):
"""{ref}`nonebot.adapters._bot.Bot` 参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["BotParam"]:
if param.default == param.empty:
if generic_check_issubclass(param.annotation, Bot):
if param.annotation is not Bot:
dependent.pre_checkers.append(
_BotChecker(
Required,
field=ModelField(
name=name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
),
)
)
return cls(Required)
elif param.annotation == param.empty and name == "bot":
return cls(Required)
async def _solve(self, bot: Bot, **kwargs: Any) -> Any:
return bot
class _EventChecker(Param):
async def _solve(self, event: Event, **kwargs: Any) -> Any:
field: ModelField = self.extra["field"]
try:
return check_field_type(field, event)
except TypeMisMatch:
logger.debug(
f"Event type {type(event)} not match "
f"annotation {field._type_display()}, ignored"
)
raise
class EventParam(Param):
"""{ref}`nonebot.adapters._event.Event` 参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["EventParam"]:
if param.default == param.empty:
if generic_check_issubclass(param.annotation, Event):
if param.annotation is not Event:
dependent.pre_checkers.append(
_EventChecker(
Required,
field=ModelField(
name=name,
type_=param.annotation,
class_validators=None,
model_config=CustomConfig,
default=None,
required=True,
),
)
)
return cls(Required)
elif param.annotation == param.empty and name == "event":
return cls(Required)
async def _solve(self, event: Event, **kwargs: Any) -> Any:
return event
async def _event_type(event: Event) -> str: async def _event_type(event: Event) -> str:
@ -265,7 +41,7 @@ async def _event_type(event: Event) -> str:
def EventType() -> str: def EventType() -> str:
"""{ref}`nonebot.adapters._event.Event` 类型参数""" """{ref}`nonebot.adapters.Event` 类型参数"""
return Depends(_event_type) return Depends(_event_type)
@ -274,7 +50,7 @@ async def _event_message(event: Event) -> Message:
def EventMessage() -> Any: def EventMessage() -> Any:
"""{ref}`nonebot.adapters._event.Event` 消息参数""" """{ref}`nonebot.adapters.Event` 消息参数"""
return Depends(_event_message) return Depends(_event_message)
@ -283,7 +59,7 @@ async def _event_plain_text(event: Event) -> str:
def EventPlainText() -> str: def EventPlainText() -> str:
"""{ref}`nonebot.adapters._event.Event` 纯文本消息参数""" """{ref}`nonebot.adapters.Event` 纯文本消息参数"""
return Depends(_event_plain_text) return Depends(_event_plain_text)
@ -292,39 +68,10 @@ async def _event_to_me(event: Event) -> bool:
def EventToMe() -> bool: def EventToMe() -> bool:
"""{ref}`nonebot.adapters._event.Event` `to_me` 参数""" """{ref}`nonebot.adapters.Event` `to_me` 参数"""
return Depends(_event_to_me) return Depends(_event_to_me)
class StateInner(T_State):
...
def State() -> T_State:
"""**Deprecated**: 事件处理状态参数,请直接使用 {ref}`nonebot.typing.T_State`"""
warnings.warn("State() is deprecated, use `T_State` instead", DeprecationWarning)
return StateInner()
class StateParam(Param):
"""事件处理状态参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["StateParam"]:
if isinstance(param.default, StateInner):
return cls(Required)
elif param.default == param.empty:
if param.annotation is T_State:
return cls(Required)
elif param.annotation == param.empty and name == "state":
return cls(Required)
async def _solve(self, state: T_State, **kwargs: Any) -> Any:
return state
def _command(state: T_State) -> Message: def _command(state: T_State) -> Message:
return state[PREFIX_KEY][CMD_KEY] return state[PREFIX_KEY][CMD_KEY]
@ -397,22 +144,6 @@ def RegexDict() -> Dict[str, Any]:
return Depends(_regex_dict, use_cache=False) return Depends(_regex_dict, use_cache=False)
class MatcherParam(Param):
"""事件响应器实例参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["MatcherParam"]:
if generic_check_issubclass(param.annotation, Matcher) or (
param.annotation == param.empty and name == "matcher"
):
return cls(Required)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
return matcher
def Received(id: Optional[str] = None, default: Any = None) -> Any: def Received(id: Optional[str] = None, default: Any = None) -> Any:
"""`receive` 事件参数""" """`receive` 事件参数"""
@ -431,85 +162,18 @@ def LastReceived(default: Any = None) -> Any:
return Depends(_last_received, use_cache=False) return Depends(_last_received, use_cache=False)
class ArgInner:
def __init__(
self, key: Optional[str], type: Literal["message", "str", "plaintext"]
) -> None:
self.key = key
self.type = type
def Arg(key: Optional[str] = None) -> Any:
"""`got` 的 Arg 参数消息"""
return ArgInner(key, "message")
def ArgStr(key: Optional[str] = None) -> str:
"""`got` 的 Arg 参数消息文本"""
return ArgInner(key, "str") # type: ignore
def ArgPlainText(key: Optional[str] = None) -> str:
"""`got` 的 Arg 参数消息纯文本"""
return ArgInner(key, "plaintext") # type: ignore
class ArgParam(Param):
"""`got` 的 Arg 参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["ArgParam"]:
if isinstance(param.default, ArgInner):
return cls(Required, key=param.default.key or name, type=param.default.type)
async def _solve(self, matcher: "Matcher", **kwargs: Any) -> Any:
message = matcher.get_arg(self.extra["key"])
if message is None:
return message
if self.extra["type"] == "message":
return message
elif self.extra["type"] == "str":
return str(message)
else:
return message.extract_plain_text()
class ExceptionParam(Param):
"""`run_postprocessor` 的异常参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["ExceptionParam"]:
if generic_check_issubclass(param.annotation, Exception) or (
param.annotation == param.empty and name == "exception"
):
return cls(Required)
async def _solve(self, exception: Optional[Exception] = None, **kwargs: Any) -> Any:
return exception
class DefaultParam(Param):
"""默认值参数"""
@classmethod
def _check_param(
cls, dependent: Dependent, name: str, param: inspect.Parameter
) -> Optional["DefaultParam"]:
if param.default != param.empty:
return cls(param.default)
async def _solve(self, **kwargs: Any) -> Any:
return Undefined
from nonebot.matcher import Matcher
__autodoc__ = { __autodoc__ = {
"DependsInner": False, "Arg": True,
"StateInner": False, "State": True,
"ArgInner": False, "ArgStr": True,
"Depends": True,
"ArgParam": True,
"BotParam": True,
"EventParam": True,
"StateParam": True,
"DependParam": True,
"ArgPlainText": True,
"DefaultParam": True,
"MatcherParam": True,
"ExceptionParam": True,
} }

View File

@ -8,104 +8,11 @@ FrontMatter:
description: nonebot.permission 模块 description: nonebot.permission 模块
""" """
import asyncio from nonebot.params import EventType
from contextlib import AsyncExitStack
from typing import Any, Set, Tuple, Union, NoReturn, Optional, Coroutine
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.dependencies import Dependent from nonebot.internal.permission import USER as USER
from nonebot.exception import SkippedException from nonebot.internal.permission import User as User
from nonebot.typing import T_DependencyCache, T_PermissionChecker from nonebot.internal.permission import Permission as Permission
from nonebot.params import BotParam, EventType, EventParam, DependParam, DefaultParam
async def _run_coro_with_catch(coro: Coroutine[Any, Any, Any]):
try:
return await coro
except SkippedException:
return False
class Permission:
"""{ref}`nonebot.matcher.Matcher` 权限类。
当事件传递时 {ref}`nonebot.matcher.Matcher` 运行前进行检查
参数:
checkers: PermissionChecker
用法:
```python
Permission(async_function) | sync_function
# 等价于
Permission(async_function, sync_function)
```
"""
__slots__ = ("checkers",)
HANDLER_PARAM_TYPES = [
DependParam,
BotParam,
EventParam,
DefaultParam,
]
def __init__(self, *checkers: Union[T_PermissionChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = set(
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
for checker in checkers
)
"""存储 `PermissionChecker`"""
async def __call__(
self,
bot: Bot,
event: Event,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否满足某个权限
参数:
bot: Bot 对象
event: Event 对象
stack: 异步上下文栈
dependency_cache: 依赖缓存
"""
if not self.checkers:
return True
results = await asyncio.gather(
*(
_run_coro_with_catch(
checker(
bot=bot,
event=event,
stack=stack,
dependency_cache=dependency_cache,
)
)
for checker in self.checkers
),
)
return any(results)
def __and__(self, other) -> NoReturn:
raise RuntimeError("And operation between Permissions is not allowed.")
def __or__(
self, other: Optional[Union["Permission", T_PermissionChecker]]
) -> "Permission":
if other is None:
return self
elif isinstance(other, Permission):
return Permission(*self.checkers, *other.checkers)
else:
return Permission(*self.checkers, other)
class Message: class Message:
@ -166,40 +73,6 @@ METAEVENT: Permission = Permission(MetaEvent())
""" """
class User:
"""检查当前事件是否属于指定会话
参数:
users: 会话 ID 元组
perm: 需同时满足的权限
"""
__slots__ = ("users", "perm")
def __init__(
self, users: Tuple[str, ...], perm: Optional[Permission] = None
) -> None:
self.users = users
self.perm = perm
async def __call__(self, bot: Bot, event: Event) -> bool:
return bool(
event.get_session_id() in self.users
and (self.perm is None or await self.perm(bot, event))
)
def USER(*users: str, perm: Optional[Permission] = None):
"""匹配当前事件属于指定会话
参数:
user: 会话白名单
perm: 需要同时满足的权限
"""
return Permission(User(users, perm))
class SuperUser: class SuperUser:
"""检查当前事件是否是消息事件且属于超级管理员""" """检查当前事件是否是消息事件且属于超级管理员"""
@ -216,4 +89,9 @@ class SuperUser:
SUPERUSER: Permission = Permission(SuperUser()) SUPERUSER: Permission = Permission(SuperUser())
"""匹配任意超级用户消息类型事件""" """匹配任意超级用户消息类型事件"""
__autodoc__ = {"Permission.__call__": True} __autodoc__ = {
"Permission": True,
"Permission.__call__": True,
"User": True,
"USER": True,
}

View File

@ -10,22 +10,28 @@ FrontMatter:
import re import re
import shlex import shlex
import asyncio
from itertools import product from itertools import product
from argparse import Namespace from argparse import Namespace
from contextlib import AsyncExitStack
from typing_extensions import TypedDict from typing_extensions import TypedDict
from argparse import ArgumentParser as ArgParser from argparse import ArgumentParser as ArgParser
from typing import Any, Set, List, Tuple, Union, NoReturn, Optional, Sequence from typing import Any, List, Tuple, Union, Optional, Sequence
from pygtrie import CharTrie from pygtrie import CharTrie
from nonebot import get_driver from nonebot import get_driver
from nonebot.log import logger from nonebot.log import logger
from nonebot.dependencies import Dependent from nonebot.typing import T_State
from nonebot.exception import ParserExit, SkippedException from nonebot.exception import ParserExit
from nonebot.internal.rule import Rule as Rule
from nonebot.adapters import Bot, Event, Message, MessageSegment from nonebot.adapters import Bot, Event, Message, MessageSegment
from nonebot.typing import T_State, T_RuleChecker, T_DependencyCache from nonebot.params import (
Command,
EventToMe,
EventType,
CommandArg,
EventMessage,
EventPlainText,
)
from nonebot.consts import ( from nonebot.consts import (
CMD_KEY, CMD_KEY,
PREFIX_KEY, PREFIX_KEY,
@ -37,19 +43,6 @@ from nonebot.consts import (
REGEX_GROUP, REGEX_GROUP,
REGEX_MATCHED, REGEX_MATCHED,
) )
from nonebot.params import (
Command,
BotParam,
EventToMe,
EventType,
CommandArg,
EventParam,
StateParam,
DependParam,
DefaultParam,
EventMessage,
EventPlainText,
)
CMD_RESULT = TypedDict( CMD_RESULT = TypedDict(
"CMD_RESULT", "CMD_RESULT",
@ -61,91 +54,6 @@ CMD_RESULT = TypedDict(
) )
class Rule:
"""{ref}`nonebot.matcher.Matcher` 规则类。
当事件传递时 {ref}`nonebot.matcher.Matcher` 运行前进行检查
参数:
*checkers: RuleChecker
用法:
```python
Rule(async_function) & sync_function
# 等价于
Rule(async_function, sync_function)
```
"""
__slots__ = ("checkers",)
HANDLER_PARAM_TYPES = [
DependParam,
BotParam,
EventParam,
StateParam,
DefaultParam,
]
def __init__(self, *checkers: Union[T_RuleChecker, Dependent[bool]]) -> None:
self.checkers: Set[Dependent[bool]] = set(
checker
if isinstance(checker, Dependent)
else Dependent[bool].parse(
call=checker, allow_types=self.HANDLER_PARAM_TYPES
)
for checker in checkers
)
"""存储 `RuleChecker`"""
async def __call__(
self,
bot: Bot,
event: Event,
state: T_State,
stack: Optional[AsyncExitStack] = None,
dependency_cache: Optional[T_DependencyCache] = None,
) -> bool:
"""检查是否符合所有规则
参数:
bot: Bot 对象
event: Event 对象
state: 当前 State
stack: 异步上下文栈
dependency_cache: 依赖缓存
"""
if not self.checkers:
return True
try:
results = await asyncio.gather(
*(
checker(
bot=bot,
event=event,
state=state,
stack=stack,
dependency_cache=dependency_cache,
)
for checker in self.checkers
)
)
except SkippedException:
return False
return all(results)
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
if other is None:
return self
elif isinstance(other, Rule):
return Rule(*self.checkers, *other.checkers)
else:
return Rule(*self.checkers, other)
def __or__(self, other) -> NoReturn:
raise RuntimeError("Or operation between rules is not allowed.")
class TrieRule: class TrieRule:
prefix: CharTrie = CharTrie() prefix: CharTrie = CharTrie()
@ -551,6 +459,7 @@ def to_me() -> Rule:
__autodoc__ = { __autodoc__ = {
"Rule": True,
"Rule.__call__": True, "Rule.__call__": True,
"TrieRule": False, "TrieRule": False,
"ArgumentParser.exit": False, "ArgumentParser.exit": False,

View File

@ -44,9 +44,9 @@ def overrides(InterfaceClass: object) -> Callable[[T_Wrapped], T_Wrapped]:
T_State = Dict[Any, Any] T_State = Dict[Any, Any]
"""事件处理状态 State 类型""" """事件处理状态 State 类型"""
T_BotConnectionHook = Callable[["Bot"], Awaitable[Any]] T_BotConnectionHook = Callable[..., Awaitable[Any]]
"""Bot 连接建立时钩子函数""" """Bot 连接建立时钩子函数"""
T_BotDisconnectionHook = Callable[["Bot"], Awaitable[Any]] T_BotDisconnectionHook = Callable[..., Awaitable[Any]]
"""Bot 连接断开时钩子函数""" """Bot 连接断开时钩子函数"""
T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]] T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]]
"""`bot.call_api` 钩子函数""" """`bot.call_api` 钩子函数"""

View File

@ -17,21 +17,6 @@ nonebot2 = "^2.0.0-beta.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
[tool.black]
line-length = 88
target-version = ["py37", "py38", "py39"]
include = '\.pyi?$'
extend-exclude = '''
'''
[tool.isort]
profile = "black"
line_length = 80
length_sort = true
skip_gitignore = true
force_sort_within_sections = true
extra_standard_library = ["typing_extensions"]
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"

123
poetry.lock generated
View File

@ -218,7 +218,7 @@ python-versions = ">=3.6.1"
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "2.0.10" version = "2.0.11"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main" category = "main"
optional = false optional = false
@ -249,7 +249,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "6.3" version = "6.3.1"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
category = "dev" category = "dev"
optional = false optional = false
@ -348,7 +348,7 @@ python-versions = ">=3.6.1"
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "0.14.5" version = "0.14.7"
description = "A minimal low-level HTTP client." description = "A minimal low-level HTTP client."
category = "main" category = "main"
optional = true optional = true
@ -428,7 +428,7 @@ python-versions = ">=3.6.1"
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.4.6" version = "2.4.8"
description = "File identification library for Python" description = "File identification library for Python"
category = "dev" category = "dev"
optional = false optional = false
@ -835,7 +835,7 @@ python-versions = ">=3.6"
[[package]] [[package]]
name = "quart" name = "quart"
version = "0.16.2" version = "0.16.3"
description = "A Python ASGI web microframework with the same API as Flask" description = "A Python ASGI web microframework with the same API as Flask"
category = "main" category = "main"
optional = true optional = true
@ -974,7 +974,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.17.1" version = "0.17.4"
description = "The lightning-fast ASGI server." description = "The lightning-fast ASGI server."
category = "main" category = "main"
optional = false optional = false
@ -1011,7 +1011,7 @@ test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.13.0" version = "20.13.1"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
category = "dev" category = "dev"
optional = false optional = false
@ -1376,8 +1376,8 @@ cfgv = [
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
] ]
charset-normalizer = [ charset-normalizer = [
{file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, {file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"},
{file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, {file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"},
] ]
click = [ click = [
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
@ -1388,50 +1388,47 @@ colorama = [
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
] ]
coverage = [ coverage = [
{file = "coverage-6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7"}, {file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"},
{file = "coverage-6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d"}, {file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"},
{file = "coverage-6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01"}, {file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"},
{file = "coverage-6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260"}, {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"},
{file = "coverage-6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a"}, {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"},
{file = "coverage-6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d"}, {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"},
{file = "coverage-6.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53"}, {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"},
{file = "coverage-6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5"}, {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"},
{file = "coverage-6.3-cp310-cp310-win32.whl", hash = "sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e"}, {file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"},
{file = "coverage-6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2"}, {file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"},
{file = "coverage-6.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30"}, {file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"},
{file = "coverage-6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92"}, {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"},
{file = "coverage-6.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d"}, {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"},
{file = "coverage-6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb"}, {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"},
{file = "coverage-6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc"}, {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"},
{file = "coverage-6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7"}, {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"},
{file = "coverage-6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f"}, {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"},
{file = "coverage-6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"}, {file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"},
{file = "coverage-6.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d"}, {file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"},
{file = "coverage-6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318"}, {file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"},
{file = "coverage-6.3-cp37-cp37m-win32.whl", hash = "sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749"}, {file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"},
{file = "coverage-6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6"}, {file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"},
{file = "coverage-6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f"}, {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"},
{file = "coverage-6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2"}, {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"},
{file = "coverage-6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5"}, {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"},
{file = "coverage-6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde"}, {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"},
{file = "coverage-6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d"}, {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"},
{file = "coverage-6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c"}, {file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"},
{file = "coverage-6.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69"}, {file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"},
{file = "coverage-6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6"}, {file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"},
{file = "coverage-6.3-cp38-cp38-win32.whl", hash = "sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89"}, {file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"},
{file = "coverage-6.3-cp38-cp38-win_amd64.whl", hash = "sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6"}, {file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"},
{file = "coverage-6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606"}, {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"},
{file = "coverage-6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196"}, {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"},
{file = "coverage-6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c"}, {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"},
{file = "coverage-6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76"}, {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"},
{file = "coverage-6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48"}, {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"},
{file = "coverage-6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0"}, {file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"},
{file = "coverage-6.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7"}, {file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"},
{file = "coverage-6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2"}, {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"},
{file = "coverage-6.3-cp39-cp39-win32.whl", hash = "sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c"}, {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"},
{file = "coverage-6.3-cp39-cp39-win_amd64.whl", hash = "sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d"},
{file = "coverage-6.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab"},
{file = "coverage-6.3.tar.gz", hash = "sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099"},
] ]
distlib = [ distlib = [
{file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
@ -1523,8 +1520,8 @@ hpack = [
{file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
] ]
httpcore = [ httpcore = [
{file = "httpcore-0.14.5-py3-none-any.whl", hash = "sha256:2621ee769d0236574df51b305c5f4c69ca8f0c7b215221ad247b1ee42a9a9de1"}, {file = "httpcore-0.14.7-py3-none-any.whl", hash = "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade"},
{file = "httpcore-0.14.5.tar.gz", hash = "sha256:435ab519628a6e2393f67812dea3ca5c6ad23b457412cd119295d9f906d96a2b"}, {file = "httpcore-0.14.7.tar.gz", hash = "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"},
] ]
httptools = [ httptools = [
{file = "httptools-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4137137de8976511a392e27bfdcf231bd926ac13d375e0414e927b08217d779e"}, {file = "httptools-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4137137de8976511a392e27bfdcf231bd926ac13d375e0414e927b08217d779e"},
@ -1565,8 +1562,8 @@ hyperframe = [
{file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"},
] ]
identify = [ identify = [
{file = "identify-2.4.6-py2.py3-none-any.whl", hash = "sha256:cf06b1639e0dca0c184b1504d8b73448c99a68e004a80524c7923b95f7b6837c"}, {file = "identify-2.4.8-py2.py3-none-any.whl", hash = "sha256:a55bdd671b6063eb837af938c250ec00bba6e610454265133b0d2db7ae718d0f"},
{file = "identify-2.4.6.tar.gz", hash = "sha256:233679e3f61a02015d4293dbccf16aa0e4996f868bd114688b8c124f18826706"}, {file = "identify-2.4.8.tar.gz", hash = "sha256:97e839c1779f07011b84c92af183e1883d9745d532d83412cca1ca76d3808c1c"},
] ]
idna = [ idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
@ -1920,8 +1917,8 @@ pyyaml = [
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
] ]
quart = [ quart = [
{file = "Quart-0.16.2-py3-none-any.whl", hash = "sha256:c1bcc4989c7e0b6c3301df10b6285c09ee0a1d75fa4cebd936f0d17312294e36"}, {file = "Quart-0.16.3-py3-none-any.whl", hash = "sha256:556d07f24a8789db3b2dca78e0fe764c5a97a75ca800b1b7e5c4cfb7c3da2ea1"},
{file = "Quart-0.16.2.tar.gz", hash = "sha256:356f4fd795fbf5a7a97bdeb7ca908b5051d0e6e4c0499b2b7c30b743a6938a7e"}, {file = "Quart-0.16.3.tar.gz", hash = "sha256:16521d8cf062461b158433d820fff509f98fb997ae6c28740eda061d9cba7d5e"},
] ]
requests = [ requests = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
@ -1990,8 +1987,8 @@ urllib3 = [
{file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"},
] ]
uvicorn = [ uvicorn = [
{file = "uvicorn-0.17.1-py3-none-any.whl", hash = "sha256:8b16d9ecb76500f7804184f182835fe8a2b54716d3b0b6bb2da0b2b192f62c73"}, {file = "uvicorn-0.17.4-py3-none-any.whl", hash = "sha256:e85872d84fb651cccc4c5d2a71cf7ead055b8fb4d8f1e78e36092282c0cf2aec"},
{file = "uvicorn-0.17.1.tar.gz", hash = "sha256:dffbacb8cc25d924d68d231d2c478c4fe6727c36537d8de21e5de591b37afc41"}, {file = "uvicorn-0.17.4.tar.gz", hash = "sha256:25850bbc86195a71a6477b3e4b3b7b4c861fb687fb96912972ce5324472b1011"},
] ]
uvloop = [ uvloop = [
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"}, {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"},
@ -2012,8 +2009,8 @@ uvloop = [
{file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"}, {file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"},
] ]
virtualenv = [ virtualenv = [
{file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"},
{file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"}, {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"},
] ]
watchgod = [ watchgod = [
{file = "watchgod-0.7-py3-none-any.whl", hash = "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"}, {file = "watchgod-0.7-py3-none-any.whl", hash = "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"},

View File

@ -16,7 +16,7 @@ options:
向用户回复一条消息。回复的方式或途径由协议适配器自行实现。 向用户回复一条消息。回复的方式或途径由协议适配器自行实现。
可以是 `str`, [`Message`](../../api/adapters/_message.md#Message), [`MessageSegment`](../../api/adapters/_message.md#MessageSegment) 或 [`MessageTemplate`](../../api/adapters/_template.md#MessageTemplate)。 可以是 `str`, [`Message`](../../api/adapters/index.md#Message), [`MessageSegment`](../../api/adapters/index.md#MessageSegment) 或 [`MessageTemplate`](../../api/adapters/index.md#MessageTemplate)。
这个操作等同于使用 `bot.send(event, message, **kwargs)` 但不需要自行传入 `event` 这个操作等同于使用 `bot.send(event, message, **kwargs)` 但不需要自行传入 `event`

View File

@ -14,6 +14,8 @@ description: Changelog
- 修改 `load_builtin_plugins` 函数,使其能够支持加载多个内置插件 - 修改 `load_builtin_plugins` 函数,使其能够支持加载多个内置插件
- 新增 `load_builtin_plugin` 函数,用于加载单个内置插件 - 新增 `load_builtin_plugin` 函数,用于加载单个内置插件
- 修改 `Message``MessageSegment` 类,完善 typing转移 Mapping 构建支持至 pydantic validate - 修改 `Message``MessageSegment` 类,完善 typing转移 Mapping 构建支持至 pydantic validate
- 调整项目结构,分离内部定义与用户接口
- 新增 Bot 连接事件钩子 (如 `driver.on_bot_connect` ) 的依赖注入
## v2.0.0b1 ## v2.0.0b1