diff --git a/nonebot/__init__.py b/nonebot/__init__.py index 123d49e9..89fb4850 100644 --- a/nonebot/__init__.py +++ b/nonebot/__init__.py @@ -122,16 +122,16 @@ def get_asgi() -> Any: 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]` 的简写; - 当不提供时,返回一个 {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 不存在 @@ -156,10 +156,10 @@ def get_bot(self_id: Optional[str] = None) -> 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 ` 尚未调用) @@ -258,7 +258,6 @@ def run(*args: Any, **kwargs: Any) -> None: get_driver().run(*args, **kwargs) -import nonebot.params as params from nonebot.plugin import on as on from nonebot.plugin import export as export 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 load_builtin_plugin as load_builtin_plugin from nonebot.plugin import load_builtin_plugins as load_builtin_plugins + +__autodoc__ = {"internal": False} diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index 1bb0bf87..0adff3e6 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -22,17 +22,18 @@ except ImportError: except Exception: pass -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 +from nonebot.internal.adapter import Bot as Bot +from nonebot.internal.adapter import Event as Event +from nonebot.internal.adapter import Adapter as Adapter +from nonebot.internal.adapter import Message as Message +from nonebot.internal.adapter import MessageSegment as MessageSegment +from nonebot.internal.adapter import MessageTemplate as MessageTemplate __autodoc__ = { - "_bot": True, - "_event": True, - "_adapter": True, - "_message": True, - "_template": True, + "Bot": True, + "Event": True, + "Adapter": True, + "Message": True, + "MessageSegment": True, + "MessageTemplate": True, } diff --git a/nonebot/drivers/__init__.py b/nonebot/drivers/__init__.py index 69bc0e6d..5c6b3ab1 100644 --- a/nonebot/drivers/__init__.py +++ b/nonebot/drivers/__init__.py @@ -7,259 +7,32 @@ FrontMatter: description: nonebot.drivers 模块 """ -import abc -import asyncio -from dataclasses import dataclass -from contextlib import asynccontextmanager -from typing import ( - TYPE_CHECKING, - Any, - Set, - Dict, - Type, - Callable, - Awaitable, - AsyncGenerator, -) +from nonebot.internal.driver import URL as URL +from nonebot.internal.driver import Driver as Driver +from nonebot.internal.driver import Cookies as Cookies +from nonebot.internal.driver import Request as Request +from nonebot.internal.driver import Response as Response +from nonebot.internal.driver import WebSocket as WebSocket +from nonebot.internal.driver import HTTPVersion as HTTPVersion +from nonebot.internal.driver import ForwardMixin as ForwardMixin +from nonebot.internal.driver import ForwardDriver as ForwardDriver +from nonebot.internal.driver import ReverseDriver as ReverseDriver +from nonebot.internal.driver import combine_driver as combine_driver +from nonebot.internal.driver import HTTPServerSetup as HTTPServerSetup +from nonebot.internal.driver import WebSocketServerSetup as WebSocketServerSetup -from nonebot.log import logger -from nonebot.utils import escape_tag -from nonebot.config import Env, Config -from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook - -from ._model import URL as URL -from ._model import Request as Request -from ._model import Response as Response -from ._model import WebSocket as WebSocket -from ._model import HTTPVersion as HTTPVersion - -if TYPE_CHECKING: - from nonebot.adapters import Bot, Adapter - - -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 "{escape_tag(name)}" already exists' - ) - return - self._adapters[name] = adapter(self, **kwargs) - logger.opt(colors=True).debug( - f'Succeeded to load adapter "{escape_tag(name)}"' - ) - - @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"Loaded adapters: {escape_tag(', '.join(self._adapters))}" - ) - - @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( - "Error when running WebSocketConnection hook. " - "Running cancelled!" - ) - - 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( - "Error when running WebSocketDisConnection hook. " - "Running cancelled!" - ) - - 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]] +__autodoc__ = { + "URL": True, + "Driver": True, + "Cookies": True, + "Request": True, + "Response": True, + "WebSocket": True, + "HTTPVersion": True, + "ForwardMixin": True, + "ForwardDriver": True, + "ReverseDriver": True, + "combine_driver": True, + "HTTPServerSetup": True, + "WebSocketServerSetup": True, +} diff --git a/nonebot/drivers/fastapi.py b/nonebot/drivers/fastapi.py index 5f08fa77..e714300a 100644 --- a/nonebot/drivers/fastapi.py +++ b/nonebot/drivers/fastapi.py @@ -22,13 +22,12 @@ from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect from nonebot.config import Env from nonebot.typing import overrides from nonebot.exception import WebSocketClosed +from nonebot.internal.driver import FileTypes from nonebot.config import Config as NoneBotConfig from nonebot.drivers import Request as BaseRequest from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup -from ._model import FileTypes - def catch_closed(func): @wraps(func) diff --git a/nonebot/drivers/quart.py b/nonebot/drivers/quart.py index 2f610064..56c940f5 100644 --- a/nonebot/drivers/quart.py +++ b/nonebot/drivers/quart.py @@ -25,13 +25,12 @@ from pydantic import BaseSettings from nonebot.config import Env from nonebot.typing import overrides from nonebot.exception import WebSocketClosed +from nonebot.internal.driver import FileTypes from nonebot.config import Config as NoneBotConfig from nonebot.drivers import Request as BaseRequest from nonebot.drivers import WebSocket as BaseWebSocket from nonebot.drivers import ReverseDriver, HTTPServerSetup, WebSocketServerSetup -from ._model import FileTypes - try: from quart import request as _request from quart import websocket as _websocket diff --git a/nonebot/exception.py b/nonebot/exception.py index 59414168..2f390843 100644 --- a/nonebot/exception.py +++ b/nonebot/exception.py @@ -202,7 +202,7 @@ class AdapterException(NoneBotException): class NoLogException(AdapterException): """指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。 - 可在 {ref}`nonebot.adapters._event.Event.get_log_string` 时抛出 + 可在 {ref}`nonebot.adapters.Event.get_log_string` 时抛出 """ diff --git a/nonebot/internal/__init__.py b/nonebot/internal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nonebot/internal/adapter/__init__.py b/nonebot/internal/adapter/__init__.py new file mode 100644 index 00000000..fc54a3e0 --- /dev/null +++ b/nonebot/internal/adapter/__init__.py @@ -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 diff --git a/nonebot/adapters/_adapter.py b/nonebot/internal/adapter/adapter.py similarity index 84% rename from nonebot/adapters/_adapter.py rename to nonebot/internal/adapter/adapter.py index 423a4bb3..9f6c5e65 100644 --- a/nonebot/adapters/_adapter.py +++ b/nonebot/internal/adapter/adapter.py @@ -1,14 +1,9 @@ -""" -FrontMatter: - sidebar_position: 1 - description: nonebot.adapters._adapter 模块 -""" import abc from contextlib import asynccontextmanager from typing import Any, Dict, AsyncGenerator from nonebot.config import Config -from nonebot.drivers import ( +from nonebot.internal.driver import ( Driver, Request, Response, @@ -19,7 +14,7 @@ from nonebot.drivers import ( WebSocketServerSetup, ) -from ._bot import Bot +from .bot import Bot class Adapter(abc.ABC): @@ -36,7 +31,7 @@ class Adapter(abc.ABC): self.driver: Driver = driver """{ref}`nonebot.drivers.Driver` 实例""" self.bots: Dict[str, Bot] = {} - """本协议适配器已建立连接的 {ref}`nonebot.adapters._bot.Bot` 实例""" + """本协议适配器已建立连接的 {ref}`nonebot.adapters.Bot` 实例""" @classmethod @abc.abstractmethod @@ -50,23 +45,23 @@ class Adapter(abc.ABC): return self.driver.config 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.bots[bot.self_id] = bot 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.bots.pop(bot.self_id, None) diff --git a/nonebot/adapters/_bot.py b/nonebot/internal/adapter/bot.py similarity index 95% rename from nonebot/adapters/_bot.py rename to nonebot/internal/adapter/bot.py index a9e9c51f..6d9a0074 100644 --- a/nonebot/adapters/_bot.py +++ b/nonebot/internal/adapter/bot.py @@ -1,8 +1,3 @@ -""" -FrontMatter: - sidebar_position: 2 - description: nonebot.adapters._bot 模块 -""" import abc import asyncio from functools import partial @@ -15,9 +10,9 @@ from nonebot.exception import MockApiException from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook if TYPE_CHECKING: - from ._event import Event - from ._adapter import Adapter - from ._message import Message, MessageSegment + from .event import Event + from .adapter import Adapter + from .message import Message, MessageSegment class _ApiCall(Protocol): diff --git a/nonebot/adapters/_event.py b/nonebot/internal/adapter/event.py similarity index 94% rename from nonebot/adapters/_event.py rename to nonebot/internal/adapter/event.py index 6dd4d141..8ef3123b 100644 --- a/nonebot/adapters/_event.py +++ b/nonebot/internal/adapter/event.py @@ -1,15 +1,10 @@ -""" -FrontMatter: - sidebar_position: 3 - description: nonebot.adapters._event 模块 -""" import abc from pydantic import BaseModel from nonebot.utils import DataclassEncoder -from ._message import Message +from .message import Message class Event(abc.ABC, BaseModel): diff --git a/nonebot/adapters/_message.py b/nonebot/internal/adapter/message.py similarity index 98% rename from nonebot/adapters/_message.py rename to nonebot/internal/adapter/message.py index ffeb2bf5..4a87648b 100644 --- a/nonebot/adapters/_message.py +++ b/nonebot/internal/adapter/message.py @@ -1,8 +1,3 @@ -""" -FrontMatter: - sidebar_position: 4 - description: nonebot.adapters._message 模块 -""" import abc from copy import deepcopy from dataclasses import field, asdict, dataclass @@ -22,7 +17,7 @@ from typing import ( from pydantic import parse_obj_as -from ._template import MessageTemplate +from .template import MessageTemplate T = TypeVar("T") TMS = TypeVar("TMS", bound="MessageSegment") diff --git a/nonebot/adapters/_template.py b/nonebot/internal/adapter/template.py similarity index 97% rename from nonebot/adapters/_template.py rename to nonebot/internal/adapter/template.py index b691cbcb..d6c934ee 100644 --- a/nonebot/adapters/_template.py +++ b/nonebot/internal/adapter/template.py @@ -1,8 +1,3 @@ -""" -FrontMatter: - sidebar_position: 5 - description: nonebot.adapters._template 模块 -""" import inspect import functools from string import Formatter @@ -26,7 +21,7 @@ from typing import ( ) if TYPE_CHECKING: - from . import Message, MessageSegment + from .message import Message, MessageSegment TM = TypeVar("TM", bound="Message") TF = TypeVar("TF", str, "Message") diff --git a/nonebot/internal/driver/__init__.py b/nonebot/internal/driver/__init__.py new file mode 100644 index 00000000..533ecfa7 --- /dev/null +++ b/nonebot/internal/driver/__init__.py @@ -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 diff --git a/nonebot/internal/driver/driver.py b/nonebot/internal/driver/driver.py new file mode 100644 index 00000000..d48b60d5 --- /dev/null +++ b/nonebot/internal/driver/driver.py @@ -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 "{escape_tag(name)}" already exists' + ) + return + self._adapters[name] = adapter(self, **kwargs) + logger.opt(colors=True).debug( + f'Succeeded to load adapter "{escape_tag(name)}"' + ) + + @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"Loaded adapters: {escape_tag(', '.join(self._adapters))}" + ) + + @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( + "Error when running WebSocketConnection hook. " + "Running cancelled!" + ) + + 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( + "Error when running WebSocketDisConnection hook. " + "Running cancelled!" + ) + + 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 diff --git a/nonebot/drivers/_model.py b/nonebot/internal/driver/model.py similarity index 94% rename from nonebot/drivers/_model.py rename to nonebot/internal/driver/model.py index 7ac198f8..b3797e4f 100644 --- a/nonebot/drivers/_model.py +++ b/nonebot/internal/driver/model.py @@ -1,5 +1,6 @@ import abc from enum import Enum +from dataclasses import dataclass from http.cookiejar import Cookie, CookieJar from typing import ( IO, @@ -9,8 +10,10 @@ from typing import ( Tuple, Union, Mapping, + Callable, Iterator, Optional, + Awaitable, MutableMapping, ) @@ -314,3 +317,22 @@ class Cookies(MutableMapping): ) return f"" + + +@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]] diff --git a/nonebot/internal/matcher.py b/nonebot/internal/matcher.py new file mode 100644 index 00000000..16ba232f --- /dev/null +++ b/nonebot/internal/matcher.py @@ -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"" + ) + + 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"" + ) + + 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, +} diff --git a/nonebot/internal/params.py b/nonebot/internal/params.py new file mode 100644 index 00000000..23fd4176 --- /dev/null +++ b/nonebot/internal/params.py @@ -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, +} diff --git a/nonebot/internal/permission.py b/nonebot/internal/permission.py new file mode 100644 index 00000000..5d43d5b2 --- /dev/null +++ b/nonebot/internal/permission.py @@ -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)) diff --git a/nonebot/internal/rule.py b/nonebot/internal/rule.py new file mode 100644 index 00000000..8d596f39 --- /dev/null +++ b/nonebot/internal/rule.py @@ -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.") diff --git a/nonebot/matcher.py b/nonebot/matcher.py index 7203d810..f747a7e6 100644 --- a/nonebot/matcher.py +++ b/nonebot/matcher.py @@ -5,697 +5,14 @@ FrontMatter: description: nonebot.matcher 模块 """ -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 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"" - ) - - 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"" - ) - - 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 - +from nonebot.internal.matcher import Matcher as Matcher +from nonebot.internal.matcher import matchers as matchers +from nonebot.internal.matcher import current_bot as current_bot +from nonebot.internal.matcher import current_event as current_event +from nonebot.internal.matcher import current_handler as current_handler +from nonebot.internal.matcher import current_matcher as current_matcher __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, + "Matcher": True, + "matchers": True, } diff --git a/nonebot/message.py b/nonebot/message.py index 47787663..4c6c3391 100644 --- a/nonebot/message.py +++ b/nonebot/message.py @@ -12,7 +12,6 @@ from datetime import datetime from contextlib import AsyncExitStack from typing import TYPE_CHECKING, Any, Set, Dict, Type, Optional, Coroutine -from nonebot import params from nonebot.log import logger from nonebot.rule import TrieRule from nonebot.utils import escape_tag @@ -32,6 +31,16 @@ from nonebot.typing import ( T_EventPreProcessor, T_EventPostProcessor, ) +from nonebot.internal.params import ( + ArgParam, + BotParam, + EventParam, + StateParam, + DependParam, + DefaultParam, + MatcherParam, + ExceptionParam, +) if TYPE_CHECKING: from nonebot.adapters import Bot, Event @@ -42,30 +51,30 @@ _run_preprocessors: Set[Dependent[Any]] = set() _run_postprocessors: Set[Dependent[Any]] = set() EVENT_PCS_PARAMS = [ - params.DependParam, - params.BotParam, - params.EventParam, - params.StateParam, - params.DefaultParam, + DependParam, + BotParam, + EventParam, + StateParam, + DefaultParam, ] RUN_PREPCS_PARAMS = [ - params.DependParam, - params.BotParam, - params.EventParam, - params.StateParam, - params.ArgParam, - params.MatcherParam, - params.DefaultParam, + DependParam, + BotParam, + EventParam, + StateParam, + ArgParam, + MatcherParam, + DefaultParam, ] RUN_POSTPCS_PARAMS = [ - params.DependParam, - params.ExceptionParam, - params.BotParam, - params.EventParam, - params.StateParam, - params.ArgParam, - params.MatcherParam, - params.DefaultParam, + DependParam, + ExceptionParam, + BotParam, + EventParam, + StateParam, + ArgParam, + MatcherParam, + DefaultParam, ] diff --git a/nonebot/params.py b/nonebot/params.py index 9334ff55..9dca0ab2 100644 --- a/nonebot/params.py +++ b/nonebot/params.py @@ -5,21 +5,24 @@ FrontMatter: description: nonebot.params 模块 """ -import asyncio -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 typing import Any, Dict, List, Tuple, Optional -from pydantic.fields import Required, Undefined, ModelField - -from nonebot.log import logger -from nonebot.exception import TypeMisMatch -from nonebot.adapters import Bot, Event, Message -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.typing import T_State +from nonebot.matcher import Matcher +from nonebot.adapters import Event, Message +from nonebot.internal.params import Arg as Arg +from nonebot.internal.params import State as State +from nonebot.internal.params import ArgStr as ArgStr +from nonebot.internal.params import Depends as Depends +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 ( CMD_KEY, PREFIX_KEY, @@ -31,233 +34,6 @@ from nonebot.consts import ( REGEX_GROUP, 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: @@ -265,7 +41,7 @@ async def _event_type(event: Event) -> str: def EventType() -> str: - """{ref}`nonebot.adapters._event.Event` 类型参数""" + """{ref}`nonebot.adapters.Event` 类型参数""" return Depends(_event_type) @@ -274,7 +50,7 @@ async def _event_message(event: Event) -> Message: def EventMessage() -> Any: - """{ref}`nonebot.adapters._event.Event` 消息参数""" + """{ref}`nonebot.adapters.Event` 消息参数""" return Depends(_event_message) @@ -283,7 +59,7 @@ async def _event_plain_text(event: Event) -> str: def EventPlainText() -> str: - """{ref}`nonebot.adapters._event.Event` 纯文本消息参数""" + """{ref}`nonebot.adapters.Event` 纯文本消息参数""" return Depends(_event_plain_text) @@ -292,39 +68,10 @@ async def _event_to_me(event: Event) -> bool: def EventToMe() -> bool: - """{ref}`nonebot.adapters._event.Event` `to_me` 参数""" + """{ref}`nonebot.adapters.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: return state[PREFIX_KEY][CMD_KEY] @@ -397,22 +144,6 @@ def RegexDict() -> Dict[str, Any]: 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: """`receive` 事件参数""" @@ -431,85 +162,18 @@ def LastReceived(default: Any = None) -> Any: 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__ = { - "DependsInner": False, - "StateInner": False, - "ArgInner": False, + "Arg": True, + "State": True, + "ArgStr": True, + "Depends": True, + "ArgParam": True, + "BotParam": True, + "EventParam": True, + "StateParam": True, + "DependParam": True, + "ArgPlainText": True, + "DefaultParam": True, + "MatcherParam": True, + "ExceptionParam": True, } diff --git a/nonebot/permission.py b/nonebot/permission.py index 20a6ffe9..1373fbb6 100644 --- a/nonebot/permission.py +++ b/nonebot/permission.py @@ -8,104 +8,11 @@ FrontMatter: description: nonebot.permission 模块 """ -import asyncio -from contextlib import AsyncExitStack -from typing import Any, Set, Tuple, Union, NoReturn, Optional, Coroutine - +from nonebot.params import EventType from nonebot.adapters import Bot, Event -from nonebot.dependencies import Dependent -from nonebot.exception import SkippedException -from nonebot.typing import T_DependencyCache, T_PermissionChecker -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) +from nonebot.internal.permission import USER as USER +from nonebot.internal.permission import User as User +from nonebot.internal.permission import Permission as Permission 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: """检查当前事件是否是消息事件且属于超级管理员""" @@ -216,4 +89,9 @@ class SuperUser: SUPERUSER: Permission = Permission(SuperUser()) """匹配任意超级用户消息类型事件""" -__autodoc__ = {"Permission.__call__": True} +__autodoc__ = { + "Permission": True, + "Permission.__call__": True, + "User": True, + "USER": True, +} diff --git a/nonebot/rule.py b/nonebot/rule.py index 479017c2..f49d38fc 100644 --- a/nonebot/rule.py +++ b/nonebot/rule.py @@ -10,22 +10,28 @@ FrontMatter: import re import shlex -import asyncio from itertools import product from argparse import Namespace -from contextlib import AsyncExitStack from typing_extensions import TypedDict 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 nonebot import get_driver from nonebot.log import logger -from nonebot.dependencies import Dependent -from nonebot.exception import ParserExit, SkippedException +from nonebot.typing import T_State +from nonebot.exception import ParserExit +from nonebot.internal.rule import Rule as Rule 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 ( CMD_KEY, PREFIX_KEY, @@ -37,19 +43,6 @@ from nonebot.consts import ( REGEX_GROUP, REGEX_MATCHED, ) -from nonebot.params import ( - Command, - BotParam, - EventToMe, - EventType, - CommandArg, - EventParam, - StateParam, - DependParam, - DefaultParam, - EventMessage, - EventPlainText, -) CMD_RESULT = TypedDict( "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: prefix: CharTrie = CharTrie() @@ -551,6 +459,7 @@ def to_me() -> Rule: __autodoc__ = { + "Rule": True, "Rule.__call__": True, "TrieRule": False, "ArgumentParser.exit": False, diff --git a/nonebot/typing.py b/nonebot/typing.py index 2c51069b..f9a62dd3 100644 --- a/nonebot/typing.py +++ b/nonebot/typing.py @@ -44,9 +44,9 @@ def overrides(InterfaceClass: object) -> Callable[[T_Wrapped], T_Wrapped]: T_State = Dict[Any, Any] """事件处理状态 State 类型""" -T_BotConnectionHook = Callable[["Bot"], Awaitable[Any]] +T_BotConnectionHook = Callable[..., Awaitable[Any]] """Bot 连接建立时钩子函数""" -T_BotDisconnectionHook = Callable[["Bot"], Awaitable[Any]] +T_BotDisconnectionHook = Callable[..., Awaitable[Any]] """Bot 连接断开时钩子函数""" T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[Any]] """`bot.call_api` 钩子函数""" diff --git a/packages/nonebot-plugin-docs/pyproject.toml b/packages/nonebot-plugin-docs/pyproject.toml index 521d6ad0..bcc1c2f9 100644 --- a/packages/nonebot-plugin-docs/pyproject.toml +++ b/packages/nonebot-plugin-docs/pyproject.toml @@ -17,21 +17,6 @@ nonebot2 = "^2.0.0-beta.1" [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] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/poetry.lock b/poetry.lock index 2fd00f86..b1fa1776 100644 --- a/poetry.lock +++ b/poetry.lock @@ -218,7 +218,7 @@ python-versions = ">=3.6.1" [[package]] 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." category = "main" optional = false @@ -249,7 +249,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.3" +version = "6.3.1" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -348,7 +348,7 @@ python-versions = ">=3.6.1" [[package]] name = "httpcore" -version = "0.14.5" +version = "0.14.7" description = "A minimal low-level HTTP client." category = "main" optional = true @@ -428,7 +428,7 @@ python-versions = ">=3.6.1" [[package]] name = "identify" -version = "2.4.6" +version = "2.4.8" description = "File identification library for Python" category = "dev" optional = false @@ -835,7 +835,7 @@ python-versions = ">=3.6" [[package]] name = "quart" -version = "0.16.2" +version = "0.16.3" description = "A Python ASGI web microframework with the same API as Flask" category = "main" optional = true @@ -974,7 +974,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" -version = "0.17.1" +version = "0.17.4" description = "The lightning-fast ASGI server." category = "main" optional = false @@ -1011,7 +1011,7 @@ test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,< [[package]] name = "virtualenv" -version = "20.13.0" +version = "20.13.1" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1376,8 +1376,8 @@ cfgv = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, - {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, + {file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"}, + {file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"}, ] click = [ {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"}, ] coverage = [ - {file = "coverage-6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7"}, - {file = "coverage-6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d"}, - {file = "coverage-6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01"}, - {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-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-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d"}, - {file = "coverage-6.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53"}, - {file = "coverage-6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5"}, - {file = "coverage-6.3-cp310-cp310-win32.whl", hash = "sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e"}, - {file = "coverage-6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2"}, - {file = "coverage-6.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30"}, - {file = "coverage-6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92"}, - {file = "coverage-6.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d"}, - {file = "coverage-6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb"}, - {file = "coverage-6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc"}, - {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-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-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"}, - {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d"}, - {file = "coverage-6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318"}, - {file = "coverage-6.3-cp37-cp37m-win32.whl", hash = "sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749"}, - {file = "coverage-6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6"}, - {file = "coverage-6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f"}, - {file = "coverage-6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2"}, - {file = "coverage-6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5"}, - {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-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-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c"}, - {file = "coverage-6.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69"}, - {file = "coverage-6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6"}, - {file = "coverage-6.3-cp38-cp38-win32.whl", hash = "sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89"}, - {file = "coverage-6.3-cp38-cp38-win_amd64.whl", hash = "sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6"}, - {file = "coverage-6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606"}, - {file = "coverage-6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196"}, - {file = "coverage-6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c"}, - {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-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-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0"}, - {file = "coverage-6.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7"}, - {file = "coverage-6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2"}, - {file = "coverage-6.3-cp39-cp39-win32.whl", hash = "sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c"}, - {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"}, + {file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"}, + {file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"}, + {file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"}, + {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.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.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"}, + {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"}, + {file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"}, + {file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"}, + {file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"}, + {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"}, + {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.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.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"}, + {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"}, + {file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"}, + {file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"}, + {file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"}, + {file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"}, + {file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"}, + {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.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.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"}, + {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"}, + {file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"}, + {file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"}, + {file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"}, + {file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"}, + {file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"}, + {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.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.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"}, + {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"}, + {file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"}, + {file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"}, + {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"}, + {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, ] distlib = [ {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"}, ] httpcore = [ - {file = "httpcore-0.14.5-py3-none-any.whl", hash = "sha256:2621ee769d0236574df51b305c5f4c69ca8f0c7b215221ad247b1ee42a9a9de1"}, - {file = "httpcore-0.14.5.tar.gz", hash = "sha256:435ab519628a6e2393f67812dea3ca5c6ad23b457412cd119295d9f906d96a2b"}, + {file = "httpcore-0.14.7-py3-none-any.whl", hash = "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade"}, + {file = "httpcore-0.14.7.tar.gz", hash = "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"}, ] httptools = [ {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"}, ] identify = [ - {file = "identify-2.4.6-py2.py3-none-any.whl", hash = "sha256:cf06b1639e0dca0c184b1504d8b73448c99a68e004a80524c7923b95f7b6837c"}, - {file = "identify-2.4.6.tar.gz", hash = "sha256:233679e3f61a02015d4293dbccf16aa0e4996f868bd114688b8c124f18826706"}, + {file = "identify-2.4.8-py2.py3-none-any.whl", hash = "sha256:a55bdd671b6063eb837af938c250ec00bba6e610454265133b0d2db7ae718d0f"}, + {file = "identify-2.4.8.tar.gz", hash = "sha256:97e839c1779f07011b84c92af183e1883d9745d532d83412cca1ca76d3808c1c"}, ] idna = [ {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"}, ] quart = [ - {file = "Quart-0.16.2-py3-none-any.whl", hash = "sha256:c1bcc4989c7e0b6c3301df10b6285c09ee0a1d75fa4cebd936f0d17312294e36"}, - {file = "Quart-0.16.2.tar.gz", hash = "sha256:356f4fd795fbf5a7a97bdeb7ca908b5051d0e6e4c0499b2b7c30b743a6938a7e"}, + {file = "Quart-0.16.3-py3-none-any.whl", hash = "sha256:556d07f24a8789db3b2dca78e0fe764c5a97a75ca800b1b7e5c4cfb7c3da2ea1"}, + {file = "Quart-0.16.3.tar.gz", hash = "sha256:16521d8cf062461b158433d820fff509f98fb997ae6c28740eda061d9cba7d5e"}, ] requests = [ {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"}, ] uvicorn = [ - {file = "uvicorn-0.17.1-py3-none-any.whl", hash = "sha256:8b16d9ecb76500f7804184f182835fe8a2b54716d3b0b6bb2da0b2b192f62c73"}, - {file = "uvicorn-0.17.1.tar.gz", hash = "sha256:dffbacb8cc25d924d68d231d2c478c4fe6727c36537d8de21e5de591b37afc41"}, + {file = "uvicorn-0.17.4-py3-none-any.whl", hash = "sha256:e85872d84fb651cccc4c5d2a71cf7ead055b8fb4d8f1e78e36092282c0cf2aec"}, + {file = "uvicorn-0.17.4.tar.gz", hash = "sha256:25850bbc86195a71a6477b3e4b3b7b4c861fb687fb96912972ce5324472b1011"}, ] uvloop = [ {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"}, ] virtualenv = [ - {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, - {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"}, + {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"}, + {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"}, ] watchgod = [ {file = "watchgod-0.7-py3-none-any.whl", hash = "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"}, diff --git a/website/docs/tutorial/plugin/matcher-operation.md b/website/docs/tutorial/plugin/matcher-operation.md index f7c4784f..2204b1ab 100644 --- a/website/docs/tutorial/plugin/matcher-operation.md +++ b/website/docs/tutorial/plugin/matcher-operation.md @@ -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`。 diff --git a/website/src/pages/changelog.md b/website/src/pages/changelog.md index 40165413..adad3335 100644 --- a/website/src/pages/changelog.md +++ b/website/src/pages/changelog.md @@ -14,6 +14,8 @@ description: Changelog - 修改 `load_builtin_plugins` 函数,使其能够支持加载多个内置插件 - 新增 `load_builtin_plugin` 函数,用于加载单个内置插件 - 修改 `Message` 和 `MessageSegment` 类,完善 typing,转移 Mapping 构建支持至 pydantic validate +- 调整项目结构,分离内部定义与用户接口 +- 新增 Bot 连接事件钩子 (如 `driver.on_bot_connect` ) 的依赖注入 ## v2.0.0b1