diff --git a/archive/2.0.0a11/advanced/runtime-hook.md b/archive/2.0.0a11/advanced/runtime-hook.md deleted file mode 100644 index b5cc8b6d..00000000 --- a/archive/2.0.0a11/advanced/runtime-hook.md +++ /dev/null @@ -1,60 +0,0 @@ -# 钩子函数 - -[`钩子编程`](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B) - -> 钩子编程(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。 - -在 `nonebot2` 中有一系列预定义的钩子函数,这些函数位于 [`nonebot.message`](https://v2.nonebot.dev/api/message.html) 模块下,我们可以以装饰器的形式利用这些函数,进行以下四种操作: - -:::warning 注意 -1.在钩子函数中,与 `matcher` 运行状态相关的函数将不可用,如 `matcher.finish()` - -2.如果需要在钩子函数中打断整个对话的执行,请参考以下范例: -```python -from nonebot.exception import IgnoredException - - -@event_preprocessor -async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): - raise IgnoredException("reason") -``` -::: - -## 事件预处理 - -```python -from nonebot.message import event_preprocessor - -@event_preprocessor -async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): - pass -``` - -## 事件后处理 - -```python -from nonebot.message import event_postprocessor - -@event_postprocessor -async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): - pass -``` - -## 运行预处理 - -```python -from nonebot.message import run_preprocessor - -@run_preprocessor -async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): - pass -``` - -## 运行后处理 -```python -from nonebot.message import run_postprocessor - -@run_postprocessor -async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): - pass -``` \ No newline at end of file diff --git a/archive/2.0.0a11/README.md b/archive/2.0.0a12/README.md similarity index 100% rename from archive/2.0.0a11/README.md rename to archive/2.0.0a12/README.md diff --git a/archive/2.0.0a11/advanced/README.md b/archive/2.0.0a12/advanced/README.md similarity index 88% rename from archive/2.0.0a11/advanced/README.md rename to archive/2.0.0a12/advanced/README.md index 6d34007c..6bdba844 100644 --- a/archive/2.0.0a11/advanced/README.md +++ b/archive/2.0.0a12/advanced/README.md @@ -10,9 +10,17 @@ **便捷起见,以下内容对 `Nonebot2` 会被称为 `nonebot`,与 `Nonebot2` 交互的机器人实现会被称为 `协议端`**。 -在实际应用中,`nonebot` 会充当一个高性能,轻量级的 Python 微服务框架。协议端可以通过 `http`, `websocket` 等方式与之通信,这个通信往往是双向的:一方面,协议端可以上报数据给 `nonebot`,`nonebot` 会处理数据并返回响应给协议端;另一方面,`nonebot` 可以主动推送数据给协议端。而 `nonebot` 便是围绕上述的双向通信进行工作的。 +在实际应用中,`nonebot` 会充当一个高性能,轻量级的 Python 微服务框架。协议端可以通过 `http`, `websocket` 等方式与之通信,这个通信往往是双向的:一方面,协议端可以上报数据给 `nonebot`,`nonebot` 会处理数据并返回响应给协议端;另一方面,`nonebot` 可以主动推送数据给协议端。而 `nonebot` 便是围绕双向通信进行工作的。 -在开始工作之前,`nonebot` 会依照**配置文件或初始化配置**启动,并会注册**协议适配器** `adapter`,之后便会加载**插件**, 随后,倘若一个协议端与 `nonebot` 进行了连接,`nonebot` 的后端驱动 `driver` 就会将 `adapter` 实例化为 `bot`,`nonebot` 便会利用 `bot` 开始工作,它的工作内容分为两个方面: +在开始工作之前,`nonebot` 需要进行准备工作: + +1. **运行 `nonebot.init` 初始化函数**,它会读取配置文件,并初始化 `nonebot` 和后端驱动 `driver` 对象。 +2. **注册协议适配器 `adapter`** 。 +3. **加载插件**。 + +准备工作完成后,`nonebot` 会利用 `uvicorn` 启动,并运行 `on_startup` 钩子函数。 + +随后,倘若一个协议端与 `nonebot` 进行了连接,`nonebot` 的后端驱动 `driver` 就会将 `adapter` 实例化为 `bot`,`nonebot` 便会利用 `bot` 开始工作,它的工作内容分为两个方面: 1. **事件处理**,`bot` 会将协议端上报的数据转化为 `事件`(`Event`),之后 `nonebot` 会根据一套既定流程来处理 `事件`。 @@ -41,7 +49,7 @@ 1. 协议端会通过 `websocket` 或者 `http` 等方式与 `nonebot` 的后端驱动 `driver` 连接,`driver` 会根据之前注册的 `adapter` 和配置文件的内容来进行鉴权,从而获得这个连接的唯一识别 id `self-id`,随后 `adapter` 就会利用 `self-id` 实例化为 `bot` 对象。 ::: tip - 需要注意的是,如果协议端通过 `websocket` 与 `nonebot` 连接,这个步骤只会在建立连接时进行;通过 `http` 方式连接时,会在协议端每次上报数据时都进行这个步骤。 + 需要注意的是,如果协议端通过 `websocket` 与 `nonebot` 连接,这个步骤只会在建立连接时进行,并在之后运行 `on_bot_connect` 钩子函数;通过 `http` 方式连接时,会在协议端每次上报数据时都进行这个步骤。 ::: ::: warning @@ -142,7 +150,7 @@ 这个异常可以在 `handler` 中由 `Matcher.reject` 抛出。 - 当 `nonebot` 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将**当前 handler 和后续 `handler`**交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行当前 `handler` 和后续的 `handler`。 + 当 `nonebot` 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将当前 handler 和后续 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行当前 `handler` 和后续的 `handler`。 4. **FinishedException** @@ -158,7 +166,7 @@ ## 调用 API -`nonebot` 可以通过 `bot` 来调用 API,API 可以向协议端发送数据,也可以向协议端请求更多的数据。 +`nonebot` 可以通过 `bot` 来调用 `API` ,`API` 可以向协议端发送数据,也可以向协议端请求更多的数据。 ::: tip 不同 `adapter` 规定了不同的 API,对应的 API 列表请参照协议规范。 diff --git a/archive/2.0.0a11/advanced/export-and-require.md b/archive/2.0.0a12/advanced/export-and-require.md similarity index 100% rename from archive/2.0.0a11/advanced/export-and-require.md rename to archive/2.0.0a12/advanced/export-and-require.md diff --git a/archive/2.0.0a11/advanced/overloaded-handlers.md b/archive/2.0.0a12/advanced/overloaded-handlers.md similarity index 100% rename from archive/2.0.0a11/advanced/overloaded-handlers.md rename to archive/2.0.0a12/advanced/overloaded-handlers.md diff --git a/archive/2.0.0a11/advanced/permission.md b/archive/2.0.0a12/advanced/permission.md similarity index 100% rename from archive/2.0.0a11/advanced/permission.md rename to archive/2.0.0a12/advanced/permission.md diff --git a/archive/2.0.0a11/advanced/publish-plugin.md b/archive/2.0.0a12/advanced/publish-plugin.md similarity index 100% rename from archive/2.0.0a11/advanced/publish-plugin.md rename to archive/2.0.0a12/advanced/publish-plugin.md diff --git a/archive/2.0.0a12/advanced/runtime-hook.md b/archive/2.0.0a12/advanced/runtime-hook.md new file mode 100644 index 00000000..f7e26be6 --- /dev/null +++ b/archive/2.0.0a12/advanced/runtime-hook.md @@ -0,0 +1,151 @@ +# 钩子函数 + +[`钩子编程`](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B) + +> 钩子编程(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。 + +在 `nonebot2` 中有一系列预定义的钩子函数,分为两类:`全局钩子函数` 和 `事件钩子函数` ,这些钩子函数可以用装饰器的形式来使用。 + +## 全局钩子函数 + +全局钩子函数是指 `nonebot2` 针对其本身运行过程的钩子函数。 + +这些钩子函数是由其后端驱动 `driver`来运行的,故需要先获得全局 `driver` 对象: + +```python +from nonebot import get_driver + + +driver=get_driver() +``` + +共分为五种函数: + +### 启动准备 + +这个钩子函数会在 `nonebot2` 启动时运行。 + +```python +@driver.on_startup +async def do_something(): + pass +``` + +### 终止处理 + +这个钩子函数会在 `nonebot2` 终止时运行。 + +```python +@driver.on_shutdown +async def do_something(): + pass +``` + +### bot 连接处理 + +这个钩子函数会在 `bot` 通过 `websocket` 连接到 `nonebot2` 时运行。 + +```python +@driver.on_bot_connect +async def do_something(bot: Bot): + pass +``` + +### bot 断开处理 + +这个钩子函数会在 `bot` 断开与 `nonebot2` 的 `websocket` 连接时运行。 + +```python +@driver.on_bot_disconnect +async def do_something(bot: Bot): + pass +``` + +### bot api 调用钩子 + +这个钩子函数会在 `Bot` 调用 API 时运行。 + +```python +from nonebot.adapters import Bot + +@Bot.on_calling_api +async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): + pass +``` + +## 事件处理钩子 + +这些钩子函数指的是影响 `nonebot2` 进行 `事件处理` 的函数。 + +:::tip 提示 + +关于 `事件处理` 的流程,可以在[这里](./README)查阅。 + +::: + +:::warning 注意 + +1.在事件处理钩子函数中,与 `matcher` 运行状态相关的函数将不可用,如 `matcher.finish()` + +2.如果需要在事件处理钩子函数中打断整个对话的执行,请参考以下范例: + +```python +from nonebot.exception import IgnoredException + + +@event_preprocessor +async def do_something(bot: Bot, event: Event, state: T_State): + raise IgnoredException("reason") +``` + +::: + +共分为四种函数: + +### 事件预处理 + +这个钩子函数会在 `Event` 上报到 `nonebot2` 时运行 + +```python +from nonebot.message import event_preprocessor + +@event_preprocessor +async def do_something(bot: Bot, event: Event, state: T_State): + pass +``` + +### 事件后处理 + +这个钩子函数会在 `nonebot2` 处理 `Event` 后运行 + +```python +from nonebot.message import event_postprocessor + +@event_postprocessor +async def do_something(bot: Bot, event: Event, state: T_State): + pass +``` + +### 运行预处理 + +这个钩子函数会在 `nonebot2`运行 `matcher` 前运行。 + +```python +from nonebot.message import run_preprocessor + +@run_preprocessor +async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): + pass +``` + +### 运行后处理 + +这个钩子函数会在 `nonebot2`运行 `matcher` 后运行。 + +```python +from nonebot.message import run_postprocessor + +@run_postprocessor +async def do_something(matcher: Matcher, exception: Optional[Exception], bot: Bot, event: Event, state: T_State): + pass +``` diff --git a/archive/2.0.0a11/advanced/scheduler.md b/archive/2.0.0a12/advanced/scheduler.md similarity index 100% rename from archive/2.0.0a11/advanced/scheduler.md rename to archive/2.0.0a12/advanced/scheduler.md diff --git a/archive/2.0.0a11/api/README.md b/archive/2.0.0a12/api/README.md similarity index 95% rename from archive/2.0.0a11/api/README.md rename to archive/2.0.0a12/api/README.md index e12dd0ff..3d5a6497 100644 --- a/archive/2.0.0a11/api/README.md +++ b/archive/2.0.0a12/api/README.md @@ -19,6 +19,9 @@ * [nonebot.matcher](matcher.html) + * [nonebot.handler](handler.html) + + * [nonebot.rule](rule.html) diff --git a/archive/2.0.0a11/api/adapters/README.md b/archive/2.0.0a12/api/adapters/README.md similarity index 90% rename from archive/2.0.0a11/api/adapters/README.md rename to archive/2.0.0a12/api/adapters/README.md index 172e87a4..bd4aab2a 100644 --- a/archive/2.0.0a11/api/adapters/README.md +++ b/archive/2.0.0a12/api/adapters/README.md @@ -27,6 +27,21 @@ Driver 对象 Config 配置对象 +### `_call_api_hook` + + +* **类型** + + `Set[T_CallingAPIHook]` + + + +* **说明** + + call_api 时执行的函数 + + + ### _abstract_ `__init__(connection_type, self_id, *, websocket=None)` @@ -93,7 +108,7 @@ Adapter 类型 * `headers: dict`: 请求头 - * `body: Optional[dict]`: 请求数据,WebSocket 连接该部分为空 + * `body: Optional[bytes]`: 请求数据,WebSocket 连接该部分为 None @@ -127,7 +142,26 @@ Adapter 类型 -### _abstract async_ `call_api(api, **data)` +### _abstract async_ `_call_api(api, **data)` + + +* **说明** + + `adapter` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。 + + + +* **参数** + + + * `api: str`: API 名称 + + + * `**data`: API 数据 + + + +### _async_ `call_api(api, **data)` * **说明** @@ -142,6 +176,9 @@ Adapter 类型 * `api: str`: API 名称 + * `self_id: Optional[str]`: 指定调用 API 的机器人 + + * `**data`: API 数据 diff --git a/archive/2.0.0a11/api/adapters/cqhttp.md b/archive/2.0.0a12/api/adapters/cqhttp.md similarity index 100% rename from archive/2.0.0a11/api/adapters/cqhttp.md rename to archive/2.0.0a12/api/adapters/cqhttp.md diff --git a/archive/2.0.0a11/api/adapters/ding.md b/archive/2.0.0a12/api/adapters/ding.md similarity index 88% rename from archive/2.0.0a11/api/adapters/ding.md rename to archive/2.0.0a12/api/adapters/ding.md index 7bb07c8a..2c531a7b 100644 --- a/archive/2.0.0a11/api/adapters/ding.md +++ b/archive/2.0.0a12/api/adapters/ding.md @@ -129,6 +129,9 @@ sidebarDepth: 0 * `api: str`: API 名称 + * `event: Optional[MessageEvent]`: Event 对象 + + * `**data: Any`: API 参数 @@ -150,7 +153,7 @@ sidebarDepth: 0 -### _async_ `send(event, message, at_sender=False, **kwargs)` +### _async_ `send(event, message, at_sender=False, webhook=None, secret=None, **kwargs)` * **说明** @@ -171,6 +174,12 @@ sidebarDepth: 0 * `at_sender: bool`: 是否 @ 事件主体 + * `webhook: Optional[str]`: 该条消息将调用的 webhook 地址。不传则将使用 sessionWebhook,若其也不存在,该条消息不发送,使用自定义 webhook 时注意你设置的安全方式,如加关键词,IP地址,加签等等。 + + + * `secret: Optional[str]`: 如果你使用自定义的 webhook 地址,推荐使用加签方式对消息进行验证,将 机器人安全设置页面,加签一栏下面显示的SEC开头的字符串 传入这个参数即可。 + + * `**kwargs`: 覆盖默认参数 diff --git a/archive/2.0.0a11/api/adapters/mirai.md b/archive/2.0.0a12/api/adapters/mirai.md similarity index 100% rename from archive/2.0.0a11/api/adapters/mirai.md rename to archive/2.0.0a12/api/adapters/mirai.md diff --git a/archive/2.0.0a11/api/config.md b/archive/2.0.0a12/api/config.md similarity index 100% rename from archive/2.0.0a11/api/config.md rename to archive/2.0.0a12/api/config.md diff --git a/archive/2.0.0a11/api/drivers/README.md b/archive/2.0.0a12/api/drivers/README.md similarity index 100% rename from archive/2.0.0a11/api/drivers/README.md rename to archive/2.0.0a12/api/drivers/README.md diff --git a/archive/2.0.0a11/api/drivers/fastapi.md b/archive/2.0.0a12/api/drivers/fastapi.md similarity index 81% rename from archive/2.0.0a11/api/drivers/fastapi.md rename to archive/2.0.0a12/api/drivers/fastapi.md index fba15c68..3b2f4f23 100644 --- a/archive/2.0.0a11/api/drivers/fastapi.md +++ b/archive/2.0.0a12/api/drivers/fastapi.md @@ -28,7 +28,7 @@ FastAPI 驱动框架设置,详情参考 FastAPI 文档 * **说明** - openapi.json 地址,默认为 None 即关闭 + `openapi.json` 地址,默认为 `None` 即关闭 @@ -43,7 +43,7 @@ FastAPI 驱动框架设置,详情参考 FastAPI 文档 * **说明** - swagger 地址,默认为 None 即关闭 + `swagger` 地址,默认为 `None` 即关闭 @@ -58,7 +58,22 @@ FastAPI 驱动框架设置,详情参考 FastAPI 文档 * **说明** - redoc 地址,默认为 None 即关闭 + `redoc` 地址,默认为 `None` 即关闭 + + + +### `fastapi_reload_dirs` + + +* **类型** + + `List[str]` + + + +* **说明** + + `debug` 模式下重载监控文件夹列表,默认为 uvicorn 默认值 diff --git a/archive/2.0.0a11/api/drivers/quart.md b/archive/2.0.0a12/api/drivers/quart.md similarity index 100% rename from archive/2.0.0a11/api/drivers/quart.md rename to archive/2.0.0a12/api/drivers/quart.md diff --git a/archive/2.0.0a11/api/exception.md b/archive/2.0.0a12/api/exception.md similarity index 100% rename from archive/2.0.0a11/api/exception.md rename to archive/2.0.0a12/api/exception.md diff --git a/archive/2.0.0a12/api/handler.md b/archive/2.0.0a12/api/handler.md new file mode 100644 index 00000000..dc2ab74f --- /dev/null +++ b/archive/2.0.0a12/api/handler.md @@ -0,0 +1,111 @@ +--- +contentSidebar: true +sidebarDepth: 0 +--- + +# NoneBot.handler 模块 + +## 事件处理函数 + +该模块实现事件处理函数的封装,以实现动态参数等功能。 + + +## _class_ `Handler` + +基类:`object` + +事件处理函数类 + + +### `__init__(func)` + +装饰事件处理函数以便根据动态参数运行 + + +### `func` + + +* **类型** + + `T_Handler` + + + +* **说明** + + 事件处理函数 + + + +### `signature` + + +* **类型** + + `inspect.Signature` + + + +* **说明** + + 事件处理函数签名 + + + +### _property_ `bot_type` + + +* **类型** + + `Union[Type["Bot"], inspect.Parameter.empty]` + + + +* **说明** + + 事件处理函数接受的 Bot 对象类型 + + + +### _property_ `event_type` + + +* **类型** + + `Optional[Union[Type[Event], inspect.Parameter.empty]]` + + + +* **说明** + + 事件处理函数接受的 event 类型 / 不需要 event 参数 + + + +### _property_ `state_type` + + +* **类型** + + `Optional[Union[T_State, inspect.Parameter.empty]]` + + + +* **说明** + + 事件处理函数是否接受 state 参数 + + + +### _property_ `matcher_type` + + +* **类型** + + `Optional[Union[Type["Matcher"], inspect.Parameter.empty]]` + + + +* **说明** + + 事件处理函数是否接受 matcher 参数 diff --git a/archive/2.0.0a11/api/log.md b/archive/2.0.0a12/api/log.md similarity index 100% rename from archive/2.0.0a11/api/log.md rename to archive/2.0.0a12/api/log.md diff --git a/archive/2.0.0a11/api/matcher.md b/archive/2.0.0a12/api/matcher.md similarity index 97% rename from archive/2.0.0a11/api/matcher.md rename to archive/2.0.0a12/api/matcher.md index 509ab6e9..0683c8f9 100644 --- a/archive/2.0.0a11/api/matcher.md +++ b/archive/2.0.0a12/api/matcher.md @@ -7,7 +7,7 @@ sidebarDepth: 0 ## 事件响应器 -该模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行 对话 。 +该模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话 。 ## `matchers` @@ -202,7 +202,7 @@ sidebarDepth: 0 * **类型** - `Optional[T_ArgsParser]` + `Optional[T_TypeUpdater]` @@ -217,7 +217,7 @@ sidebarDepth: 0 * **类型** - `Optional[T_ArgsParser]` + `Optional[T_PermissionUpdater]` @@ -237,7 +237,7 @@ sidebarDepth: 0 * **类型** - `List[T_Handler]` + `List[Handler]` diff --git a/archive/2.0.0a11/api/message.md b/archive/2.0.0a12/api/message.md similarity index 100% rename from archive/2.0.0a11/api/message.md rename to archive/2.0.0a12/api/message.md diff --git a/archive/2.0.0a11/api/nonebot.md b/archive/2.0.0a12/api/nonebot.md similarity index 100% rename from archive/2.0.0a11/api/nonebot.md rename to archive/2.0.0a12/api/nonebot.md diff --git a/archive/2.0.0a11/api/permission.md b/archive/2.0.0a12/api/permission.md similarity index 100% rename from archive/2.0.0a11/api/permission.md rename to archive/2.0.0a12/api/permission.md diff --git a/archive/2.0.0a11/api/plugin.md b/archive/2.0.0a12/api/plugin.md similarity index 91% rename from archive/2.0.0a11/api/plugin.md rename to archive/2.0.0a12/api/plugin.md index a924bb6b..ca0827ce 100644 --- a/archive/2.0.0a11/api/plugin.md +++ b/archive/2.0.0a12/api/plugin.md @@ -25,38 +25,6 @@ sidebarDepth: 0 -## _class_ `Export` - -基类:`dict` - - -* **说明** - - 插件导出内容以使得其他插件可以获得。 - - - -* **示例** - - -```python -nonebot.export().default = "bar" - -@nonebot.export() -def some_function(): - pass - -# this doesn't work before python 3.9 -# use -# export = nonebot.export(); @export.sub -# instead -# See also PEP-614: https://www.python.org/dev/peps/pep-0614/ -@nonebot.export().sub -def something_else(): - pass -``` - - ## _class_ `Plugin` 基类:`object` @@ -82,15 +50,6 @@ def something_else(): * **说明**: 插件模块对象 -### `matcher` - - -* **类型**: `Set[Type[Matcher]]` - - -* **说明**: 插件内定义的 `Matcher` - - ### `export` @@ -100,6 +59,15 @@ def something_else(): * **说明**: 插件内定义的导出内容 +### _property_ `matcher` + + +* **类型**: `Set[Type[Matcher]]` + + +* **说明**: 插件内定义的 `Matcher` + + ## `on(type='', rule=None, permission=None, *, handlers=None, temp=False, priority=1, block=False, state=None, state_factory=None)` @@ -121,7 +89,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -162,7 +130,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -206,7 +174,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -247,7 +215,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -288,7 +256,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -335,7 +303,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -382,7 +350,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -429,7 +397,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -481,7 +449,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -538,7 +506,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -590,7 +558,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -767,7 +735,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -808,7 +776,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -852,7 +820,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -893,7 +861,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -934,7 +902,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -981,7 +949,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1028,7 +996,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1075,7 +1043,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1127,7 +1095,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1184,7 +1152,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1236,7 +1204,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1442,22 +1410,6 @@ def something_else(): -## `export()` - - -* **说明** - - 获取插件的导出内容对象 - - - -* **返回** - - - * `Export` - - - ## `require(name)` @@ -1478,3 +1430,51 @@ def something_else(): * `Optional[Export]` + + + +## _class_ `Export` + +基类:`dict` + + +* **说明** + + 插件导出内容以使得其他插件可以获得。 + + + +* **示例** + + +```python +nonebot.export().default = "bar" + +@nonebot.export() +def some_function(): + pass + +# this doesn't work before python 3.9 +# use +# export = nonebot.export(); @export.sub +# instead +# See also PEP-614: https://www.python.org/dev/peps/pep-0614/ +@nonebot.export().sub +def something_else(): + pass +``` + + +## `export()` + + +* **说明** + + 获取插件的导出内容对象 + + + +* **返回** + + + * `Export` diff --git a/archive/2.0.0a11/api/rule.md b/archive/2.0.0a12/api/rule.md similarity index 100% rename from archive/2.0.0a11/api/rule.md rename to archive/2.0.0a12/api/rule.md diff --git a/archive/2.0.0a11/api/typing.md b/archive/2.0.0a12/api/typing.md similarity index 100% rename from archive/2.0.0a11/api/typing.md rename to archive/2.0.0a12/api/typing.md diff --git a/archive/2.0.0a11/api/utils.md b/archive/2.0.0a12/api/utils.md similarity index 100% rename from archive/2.0.0a11/api/utils.md rename to archive/2.0.0a12/api/utils.md diff --git a/archive/2.0.0a11/guide/README.md b/archive/2.0.0a12/guide/README.md similarity index 100% rename from archive/2.0.0a11/guide/README.md rename to archive/2.0.0a12/guide/README.md diff --git a/archive/2.0.0a11/guide/basic-configuration.md b/archive/2.0.0a12/guide/basic-configuration.md similarity index 100% rename from archive/2.0.0a11/guide/basic-configuration.md rename to archive/2.0.0a12/guide/basic-configuration.md diff --git a/archive/2.0.0a11/guide/cqhttp-guide.md b/archive/2.0.0a12/guide/cqhttp-guide.md similarity index 97% rename from archive/2.0.0a11/guide/cqhttp-guide.md rename to archive/2.0.0a12/guide/cqhttp-guide.md index 356fa6c3..867eb56f 100644 --- a/archive/2.0.0a11/guide/cqhttp-guide.md +++ b/archive/2.0.0a12/guide/cqhttp-guide.md @@ -1,5 +1,11 @@ # CQHTTP 协议使用指南 +## 安装 NoneBot CQHTTP 适配器 + +```bash +pip install nonebot-adapter-cqhttp +``` + ## 配置 CQHTTP 协议端(以 QQ 为例) 单纯运行 NoneBot 实例并不会产生任何效果,因为此刻 QQ 这边还不知道 NoneBot 的存在,也就无法把消息发送给它,因此现在需要使用一个无头 QQ 来把消息等事件上报给 NoneBot。 diff --git a/archive/2.0.0a11/guide/creating-a-handler.md b/archive/2.0.0a12/guide/creating-a-handler.md similarity index 100% rename from archive/2.0.0a11/guide/creating-a-handler.md rename to archive/2.0.0a12/guide/creating-a-handler.md diff --git a/archive/2.0.0a11/guide/creating-a-matcher.md b/archive/2.0.0a12/guide/creating-a-matcher.md similarity index 100% rename from archive/2.0.0a11/guide/creating-a-matcher.md rename to archive/2.0.0a12/guide/creating-a-matcher.md diff --git a/archive/2.0.0a11/guide/creating-a-plugin.md b/archive/2.0.0a12/guide/creating-a-plugin.md similarity index 100% rename from archive/2.0.0a11/guide/creating-a-plugin.md rename to archive/2.0.0a12/guide/creating-a-plugin.md diff --git a/archive/2.0.0a11/guide/creating-a-project.md b/archive/2.0.0a12/guide/creating-a-project.md similarity index 100% rename from archive/2.0.0a11/guide/creating-a-project.md rename to archive/2.0.0a12/guide/creating-a-project.md diff --git a/archive/2.0.0a11/guide/ding-guide.md b/archive/2.0.0a12/guide/ding-guide.md similarity index 67% rename from archive/2.0.0a11/guide/ding-guide.md rename to archive/2.0.0a12/guide/ding-guide.md index ee9449ba..cb710d26 100644 --- a/archive/2.0.0a11/guide/ding-guide.md +++ b/archive/2.0.0a12/guide/ding-guide.md @@ -11,6 +11,16 @@ - [群机器人概述](https://developers.dingtalk.com/document/app/overview-of-group-robots) - [开发企业内部机器人](https://developers.dingtalk.com/document/app/develop-enterprise-internal-robots) +钉钉官方机器人教程(Java): + +- [开发一个钉钉机器人](https://developers.dingtalk.com/document/tutorial/create-a-robot) + +## 安装 NoneBot 钉钉 适配器 + +```bash +pip install nonebot-adapter-ding +``` + ## 关于 DingAdapter 的说明 你需要显式的注册 ding 这个适配器: @@ -87,6 +97,58 @@ async def raw_handler(bot: DingBot, event: MessageEvent): 其他消息格式请查看 [钉钉适配器的 MessageSegment](https://github.com/nonebot/nonebot2/blob/dev/nonebot/adapters/ding/message.py#L8),里面封装了很多有关消息的方法,比如 `code`、`image`、`feedCard` 等。 +## 发送到特定群聊 + +钉钉也支持通过 Webhook 的方式直接将消息推送到某个群聊([参考链接](https://developers.dingtalk.com/document/app/custom-robot-access/title-zob-eyu-qse)),你可以在机器人的设置中看到当前群的 Webhook 地址。 + +![机器人所在群的 Webhook 地址](./images/ding/webhook.png) + +获取到Webhook地址后,用户可以向这个地址发起HTTP POST 请求,即可实现给该钉钉群发送消息。 + +对于这种通过 Webhook 推送的消息,钉钉需要开发者进行安全方面的设置(目前有3种安全设置方式,请根据需要选择一种),如下: + +1. **自定义关键词:** 最多可以设置10个关键词,消息中至少包含其中1个关键词才可以发送成功。 + 例如添加了一个自定义关键词:监控报警,则这个机器人所发送的消息,必须包含监控报警这个词,才能发送成功。 +2. **加签:** 发送请求时带上验签的值,可以在机器人设置里看到密钥。 + ![加签密钥](./images/ding/jiaqian.png) +3. **IP地址(段):** 设定后,只有来自IP地址范围内的请求才会被正常处理。支持两种设置方式:IP地址和IP地址段,暂不支持IPv6地址白名单。 + +如果你选择 1/3 两种安全设置,你需要自己确认当前网络和发送的消息能被钉钉接受,然后使用 `bot.send` 的时候将 webhook 地址传入 webhook 参数即可。 + +如我设置了 `打卡` 为关键词: + +```python +message = MessageSegment.text("打卡成功:XXXXXX") +await hello.send( + message, + webhook= + "https://oapi.dingtalk.com/robot/send?access_token=XXXXXXXXXXXXXX", +) +``` + +对于第二种加签方式,你可以在 `bot.send` 的时候把 `secret` 参数传进去,Nonebot 内部会自动帮你计算发送该消息的签名并发送,如: + +这里的 `secret` 参数就是加签选项给出的那个密钥。 + +```python +message = MessageSegment.raw({ + "msgtype": "text", + "text": { + "content": 'hello from webhook,一定要注意安全方式的鉴权哦,否则可能发送失败的' + }, +}) +message += MessageSegment.atDingtalkIds(event.senderId) +await hello.send( + message, + webhook="https://oapi.dingtalk.com/robot/send?access_token=XXXXXXXXXXXXXX", + secret="SECXXXXXXXXXXXXXXXXXXXXXXXXX", +) +``` + +然后就可以发送成功了。 + +![测试 Webhook 发送](images/ding/test_webhook.png) + ## 创建机器人并连接 在钉钉官方文档 [「开发企业内部机器人 -> 步骤一:创建机器人应用」](https://developers.dingtalk.com/document/app/develop-enterprise-internal-robots/title-ufs-4gh-poh) 中有详细介绍,这里就省去创建的步骤,介绍一下如何连接上程序。 diff --git a/archive/2.0.0a11/guide/end-or-start.md b/archive/2.0.0a12/guide/end-or-start.md similarity index 72% rename from archive/2.0.0a11/guide/end-or-start.md rename to archive/2.0.0a12/guide/end-or-start.md index 9587c4bb..aa072c3d 100644 --- a/archive/2.0.0a11/guide/end-or-start.md +++ b/archive/2.0.0a12/guide/end-or-start.md @@ -4,6 +4,5 @@ - 请千万注意事件处理器的优先级设定 - 在匹配规则中请勿使用耗时极长的函数 -- 同一个用户可以**跨群**(**私聊**)继续他的事件处理(除非做出权限限制,将在后续介绍) 如果「指南」还不能满足你,前往 [进阶](../advanced/README.md) 查看更多的功能信息。 diff --git a/archive/2.0.0a11/guide/getting-started.md b/archive/2.0.0a12/guide/getting-started.md similarity index 100% rename from archive/2.0.0a11/guide/getting-started.md rename to archive/2.0.0a12/guide/getting-started.md diff --git a/archive/2.0.0a11/guide/images/Handle-Event.png b/archive/2.0.0a12/guide/images/Handle-Event.png similarity index 100% rename from archive/2.0.0a11/guide/images/Handle-Event.png rename to archive/2.0.0a12/guide/images/Handle-Event.png diff --git a/archive/2.0.0a12/guide/images/ding/jiaqian.png b/archive/2.0.0a12/guide/images/ding/jiaqian.png new file mode 100644 index 00000000..8895d6c6 Binary files /dev/null and b/archive/2.0.0a12/guide/images/ding/jiaqian.png differ diff --git a/archive/2.0.0a12/guide/images/ding/test_webhook.png b/archive/2.0.0a12/guide/images/ding/test_webhook.png new file mode 100644 index 00000000..6620003d Binary files /dev/null and b/archive/2.0.0a12/guide/images/ding/test_webhook.png differ diff --git a/archive/2.0.0a12/guide/images/ding/webhook.png b/archive/2.0.0a12/guide/images/ding/webhook.png new file mode 100644 index 00000000..c957e72f Binary files /dev/null and b/archive/2.0.0a12/guide/images/ding/webhook.png differ diff --git a/archive/2.0.0a11/guide/installation.md b/archive/2.0.0a12/guide/installation.md similarity index 88% rename from archive/2.0.0a11/guide/installation.md rename to archive/2.0.0a12/guide/installation.md index aaf916ec..04e5e7af 100644 --- a/archive/2.0.0a11/guide/installation.md +++ b/archive/2.0.0a12/guide/installation.md @@ -1,6 +1,6 @@ # 安装 -## NoneBot +## 安装 NoneBot :::warning 注意 请确保你的 Python 版本 >= 3.7。 @@ -67,6 +67,19 @@ poetry install --no-dev # 推荐 pip install . # 不推荐 ``` +## 安装适配器 + +适配器可以通过 `nb-cli` 在创建项目时根据你的选择自动安装,也可以自行使用 `pip` 安装 + +```bash +pip install nonebot-adapter- +``` + +```bash +# 列出所有的适配器 +nb adapter list +``` + ## 安装插件 插件可以通过 `nb-cli` 进行安装,也可以自行安装并加载插件。 @@ -87,6 +100,7 @@ nb plugin install xxx - [NoneBot-Plugin-Docs](https://github.com/nonebot/nonebot2/tree/master/packages/nonebot-plugin-docs) 离线文档插件 - [NoneBot-Plugin-Test](https://github.com/nonebot/plugin-test) 本地机器人测试前端插件 - [NoneBot-Plugin-APScheduler](https://github.com/nonebot/plugin-apscheduler) 定时任务插件 +- [NoneBot-Plugin-LocalStore](https://github.com/nonebot/plugin-localstore) 本地数据文件存储插件 - [NoneBot-Plugin-Sentry](https://github.com/cscs181/QQ-GitHub-Bot/tree/master/src/plugins/nonebot_plugin_sentry) Sentry 在线日志分析插件 - [NoneBot-Plugin-Status](https://github.com/cscs181/QQ-GitHub-Bot/tree/master/src/plugins/nonebot_plugin_status) 服务器状态查看插件 diff --git a/archive/2.0.0a11/guide/loading-a-plugin.md b/archive/2.0.0a12/guide/loading-a-plugin.md similarity index 100% rename from archive/2.0.0a11/guide/loading-a-plugin.md rename to archive/2.0.0a12/guide/loading-a-plugin.md diff --git a/archive/2.0.0a11/guide/mirai-guide.md b/archive/2.0.0a12/guide/mirai-guide.md similarity index 99% rename from archive/2.0.0a11/guide/mirai-guide.md rename to archive/2.0.0a12/guide/mirai-guide.md index c22631e0..71003e64 100644 --- a/archive/2.0.0a11/guide/mirai-guide.md +++ b/archive/2.0.0a12/guide/mirai-guide.md @@ -28,6 +28,12 @@ Mirai-API-HTTP 的适配器以 [AGPLv3 许可](https://opensource.org/licenses/A **为了便捷起见, 以下内容均以缩写 `MAH` 代替 `mirai-api-http`** +## 安装 NoneBot Mirai 适配器 + +```bash +pip install nonebot-adapter-mirai +``` + ## 配置 MAH 客户端 正如你可能刚刚在[CQHTTP 协议使用指南](./cqhttp-guide.md)中所读到的: diff --git a/archive/2.0.0a11/sidebar.config.json b/archive/2.0.0a12/sidebar.config.json similarity index 97% rename from archive/2.0.0a11/sidebar.config.json rename to archive/2.0.0a12/sidebar.config.json index 8b16881e..97e82f74 100644 --- a/archive/2.0.0a11/sidebar.config.json +++ b/archive/2.0.0a12/sidebar.config.json @@ -119,6 +119,10 @@ "title": "nonebot.matcher 模块", "path": "matcher" }, + { + "title": "nonebot.handler 模块", + "path": "handler" + }, { "title": "nonebot.rule 模块", "path": "rule" diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index c2efeb3e..fdfac5b0 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -166,6 +166,10 @@ module.exports = context => ({ title: "nonebot.matcher 模块", path: "matcher" }, + { + title: "nonebot.handler 模块", + path: "handler" + }, { title: "nonebot.rule 模块", path: "rule" diff --git a/docs/.vuepress/versions.json b/docs/.vuepress/versions.json index a171912c..b159ca74 100644 --- a/docs/.vuepress/versions.json +++ b/docs/.vuepress/versions.json @@ -1,5 +1,5 @@ [ - "2.0.0a11", + "2.0.0a12", "2.0.0a10", "2.0.0a8.post2", "2.0.0a7" diff --git a/docs/advanced/README.md b/docs/advanced/README.md index 6d34007c..6bdba844 100644 --- a/docs/advanced/README.md +++ b/docs/advanced/README.md @@ -10,9 +10,17 @@ **便捷起见,以下内容对 `Nonebot2` 会被称为 `nonebot`,与 `Nonebot2` 交互的机器人实现会被称为 `协议端`**。 -在实际应用中,`nonebot` 会充当一个高性能,轻量级的 Python 微服务框架。协议端可以通过 `http`, `websocket` 等方式与之通信,这个通信往往是双向的:一方面,协议端可以上报数据给 `nonebot`,`nonebot` 会处理数据并返回响应给协议端;另一方面,`nonebot` 可以主动推送数据给协议端。而 `nonebot` 便是围绕上述的双向通信进行工作的。 +在实际应用中,`nonebot` 会充当一个高性能,轻量级的 Python 微服务框架。协议端可以通过 `http`, `websocket` 等方式与之通信,这个通信往往是双向的:一方面,协议端可以上报数据给 `nonebot`,`nonebot` 会处理数据并返回响应给协议端;另一方面,`nonebot` 可以主动推送数据给协议端。而 `nonebot` 便是围绕双向通信进行工作的。 -在开始工作之前,`nonebot` 会依照**配置文件或初始化配置**启动,并会注册**协议适配器** `adapter`,之后便会加载**插件**, 随后,倘若一个协议端与 `nonebot` 进行了连接,`nonebot` 的后端驱动 `driver` 就会将 `adapter` 实例化为 `bot`,`nonebot` 便会利用 `bot` 开始工作,它的工作内容分为两个方面: +在开始工作之前,`nonebot` 需要进行准备工作: + +1. **运行 `nonebot.init` 初始化函数**,它会读取配置文件,并初始化 `nonebot` 和后端驱动 `driver` 对象。 +2. **注册协议适配器 `adapter`** 。 +3. **加载插件**。 + +准备工作完成后,`nonebot` 会利用 `uvicorn` 启动,并运行 `on_startup` 钩子函数。 + +随后,倘若一个协议端与 `nonebot` 进行了连接,`nonebot` 的后端驱动 `driver` 就会将 `adapter` 实例化为 `bot`,`nonebot` 便会利用 `bot` 开始工作,它的工作内容分为两个方面: 1. **事件处理**,`bot` 会将协议端上报的数据转化为 `事件`(`Event`),之后 `nonebot` 会根据一套既定流程来处理 `事件`。 @@ -41,7 +49,7 @@ 1. 协议端会通过 `websocket` 或者 `http` 等方式与 `nonebot` 的后端驱动 `driver` 连接,`driver` 会根据之前注册的 `adapter` 和配置文件的内容来进行鉴权,从而获得这个连接的唯一识别 id `self-id`,随后 `adapter` 就会利用 `self-id` 实例化为 `bot` 对象。 ::: tip - 需要注意的是,如果协议端通过 `websocket` 与 `nonebot` 连接,这个步骤只会在建立连接时进行;通过 `http` 方式连接时,会在协议端每次上报数据时都进行这个步骤。 + 需要注意的是,如果协议端通过 `websocket` 与 `nonebot` 连接,这个步骤只会在建立连接时进行,并在之后运行 `on_bot_connect` 钩子函数;通过 `http` 方式连接时,会在协议端每次上报数据时都进行这个步骤。 ::: ::: warning @@ -142,7 +150,7 @@ 这个异常可以在 `handler` 中由 `Matcher.reject` 抛出。 - 当 `nonebot` 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将**当前 handler 和后续 `handler`**交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行当前 `handler` 和后续的 `handler`。 + 当 `nonebot` 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将当前 handler 和后续 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行当前 `handler` 和后续的 `handler`。 4. **FinishedException** @@ -158,7 +166,7 @@ ## 调用 API -`nonebot` 可以通过 `bot` 来调用 API,API 可以向协议端发送数据,也可以向协议端请求更多的数据。 +`nonebot` 可以通过 `bot` 来调用 `API` ,`API` 可以向协议端发送数据,也可以向协议端请求更多的数据。 ::: tip 不同 `adapter` 规定了不同的 API,对应的 API 列表请参照协议规范。 diff --git a/docs/advanced/runtime-hook.md b/docs/advanced/runtime-hook.md index b5cc8b6d..f7e26be6 100644 --- a/docs/advanced/runtime-hook.md +++ b/docs/advanced/runtime-hook.md @@ -4,43 +4,131 @@ > 钩子编程(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。 -在 `nonebot2` 中有一系列预定义的钩子函数,这些函数位于 [`nonebot.message`](https://v2.nonebot.dev/api/message.html) 模块下,我们可以以装饰器的形式利用这些函数,进行以下四种操作: +在 `nonebot2` 中有一系列预定义的钩子函数,分为两类:`全局钩子函数` 和 `事件钩子函数` ,这些钩子函数可以用装饰器的形式来使用。 + +## 全局钩子函数 + +全局钩子函数是指 `nonebot2` 针对其本身运行过程的钩子函数。 + +这些钩子函数是由其后端驱动 `driver`来运行的,故需要先获得全局 `driver` 对象: + +```python +from nonebot import get_driver + + +driver=get_driver() +``` + +共分为五种函数: + +### 启动准备 + +这个钩子函数会在 `nonebot2` 启动时运行。 + +```python +@driver.on_startup +async def do_something(): + pass +``` + +### 终止处理 + +这个钩子函数会在 `nonebot2` 终止时运行。 + +```python +@driver.on_shutdown +async def do_something(): + pass +``` + +### bot 连接处理 + +这个钩子函数会在 `bot` 通过 `websocket` 连接到 `nonebot2` 时运行。 + +```python +@driver.on_bot_connect +async def do_something(bot: Bot): + pass +``` + +### bot 断开处理 + +这个钩子函数会在 `bot` 断开与 `nonebot2` 的 `websocket` 连接时运行。 + +```python +@driver.on_bot_disconnect +async def do_something(bot: Bot): + pass +``` + +### bot api 调用钩子 + +这个钩子函数会在 `Bot` 调用 API 时运行。 + +```python +from nonebot.adapters import Bot + +@Bot.on_calling_api +async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): + pass +``` + +## 事件处理钩子 + +这些钩子函数指的是影响 `nonebot2` 进行 `事件处理` 的函数。 + +:::tip 提示 + +关于 `事件处理` 的流程,可以在[这里](./README)查阅。 + +::: :::warning 注意 -1.在钩子函数中,与 `matcher` 运行状态相关的函数将不可用,如 `matcher.finish()` -2.如果需要在钩子函数中打断整个对话的执行,请参考以下范例: +1.在事件处理钩子函数中,与 `matcher` 运行状态相关的函数将不可用,如 `matcher.finish()` + +2.如果需要在事件处理钩子函数中打断整个对话的执行,请参考以下范例: + ```python from nonebot.exception import IgnoredException @event_preprocessor -async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): +async def do_something(bot: Bot, event: Event, state: T_State): raise IgnoredException("reason") ``` + ::: -## 事件预处理 +共分为四种函数: + +### 事件预处理 + +这个钩子函数会在 `Event` 上报到 `nonebot2` 时运行 ```python from nonebot.message import event_preprocessor @event_preprocessor -async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): +async def do_something(bot: Bot, event: Event, state: T_State): pass ``` -## 事件后处理 +### 事件后处理 + +这个钩子函数会在 `nonebot2` 处理 `Event` 后运行 ```python from nonebot.message import event_postprocessor @event_postprocessor -async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): +async def do_something(bot: Bot, event: Event, state: T_State): pass ``` -## 运行预处理 +### 运行预处理 + +这个钩子函数会在 `nonebot2`运行 `matcher` 前运行。 ```python from nonebot.message import run_preprocessor @@ -50,11 +138,14 @@ async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State) pass ``` -## 运行后处理 +### 运行后处理 + +这个钩子函数会在 `nonebot2`运行 `matcher` 后运行。 + ```python from nonebot.message import run_postprocessor @run_postprocessor -async def do_something(matcher: Matcher, bot: Bot, event: Event, state: T_State): +async def do_something(matcher: Matcher, exception: Optional[Exception], bot: Bot, event: Event, state: T_State): pass -``` \ No newline at end of file +``` diff --git a/docs/api/README.md b/docs/api/README.md index e12dd0ff..3d5a6497 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -19,6 +19,9 @@ * [nonebot.matcher](matcher.html) + * [nonebot.handler](handler.html) + + * [nonebot.rule](rule.html) diff --git a/docs/api/adapters/README.md b/docs/api/adapters/README.md index 172e87a4..bd4aab2a 100644 --- a/docs/api/adapters/README.md +++ b/docs/api/adapters/README.md @@ -27,6 +27,21 @@ Driver 对象 Config 配置对象 +### `_call_api_hook` + + +* **类型** + + `Set[T_CallingAPIHook]` + + + +* **说明** + + call_api 时执行的函数 + + + ### _abstract_ `__init__(connection_type, self_id, *, websocket=None)` @@ -93,7 +108,7 @@ Adapter 类型 * `headers: dict`: 请求头 - * `body: Optional[dict]`: 请求数据,WebSocket 连接该部分为空 + * `body: Optional[bytes]`: 请求数据,WebSocket 连接该部分为 None @@ -127,7 +142,26 @@ Adapter 类型 -### _abstract async_ `call_api(api, **data)` +### _abstract async_ `_call_api(api, **data)` + + +* **说明** + + `adapter` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。 + + + +* **参数** + + + * `api: str`: API 名称 + + + * `**data`: API 数据 + + + +### _async_ `call_api(api, **data)` * **说明** @@ -142,6 +176,9 @@ Adapter 类型 * `api: str`: API 名称 + * `self_id: Optional[str]`: 指定调用 API 的机器人 + + * `**data`: API 数据 diff --git a/docs/api/adapters/ding.md b/docs/api/adapters/ding.md index 7bb07c8a..2c531a7b 100644 --- a/docs/api/adapters/ding.md +++ b/docs/api/adapters/ding.md @@ -129,6 +129,9 @@ sidebarDepth: 0 * `api: str`: API 名称 + * `event: Optional[MessageEvent]`: Event 对象 + + * `**data: Any`: API 参数 @@ -150,7 +153,7 @@ sidebarDepth: 0 -### _async_ `send(event, message, at_sender=False, **kwargs)` +### _async_ `send(event, message, at_sender=False, webhook=None, secret=None, **kwargs)` * **说明** @@ -171,6 +174,12 @@ sidebarDepth: 0 * `at_sender: bool`: 是否 @ 事件主体 + * `webhook: Optional[str]`: 该条消息将调用的 webhook 地址。不传则将使用 sessionWebhook,若其也不存在,该条消息不发送,使用自定义 webhook 时注意你设置的安全方式,如加关键词,IP地址,加签等等。 + + + * `secret: Optional[str]`: 如果你使用自定义的 webhook 地址,推荐使用加签方式对消息进行验证,将 机器人安全设置页面,加签一栏下面显示的SEC开头的字符串 传入这个参数即可。 + + * `**kwargs`: 覆盖默认参数 diff --git a/docs/api/handler.md b/docs/api/handler.md new file mode 100644 index 00000000..dc2ab74f --- /dev/null +++ b/docs/api/handler.md @@ -0,0 +1,111 @@ +--- +contentSidebar: true +sidebarDepth: 0 +--- + +# NoneBot.handler 模块 + +## 事件处理函数 + +该模块实现事件处理函数的封装,以实现动态参数等功能。 + + +## _class_ `Handler` + +基类:`object` + +事件处理函数类 + + +### `__init__(func)` + +装饰事件处理函数以便根据动态参数运行 + + +### `func` + + +* **类型** + + `T_Handler` + + + +* **说明** + + 事件处理函数 + + + +### `signature` + + +* **类型** + + `inspect.Signature` + + + +* **说明** + + 事件处理函数签名 + + + +### _property_ `bot_type` + + +* **类型** + + `Union[Type["Bot"], inspect.Parameter.empty]` + + + +* **说明** + + 事件处理函数接受的 Bot 对象类型 + + + +### _property_ `event_type` + + +* **类型** + + `Optional[Union[Type[Event], inspect.Parameter.empty]]` + + + +* **说明** + + 事件处理函数接受的 event 类型 / 不需要 event 参数 + + + +### _property_ `state_type` + + +* **类型** + + `Optional[Union[T_State, inspect.Parameter.empty]]` + + + +* **说明** + + 事件处理函数是否接受 state 参数 + + + +### _property_ `matcher_type` + + +* **类型** + + `Optional[Union[Type["Matcher"], inspect.Parameter.empty]]` + + + +* **说明** + + 事件处理函数是否接受 matcher 参数 diff --git a/docs/api/matcher.md b/docs/api/matcher.md index 509ab6e9..0683c8f9 100644 --- a/docs/api/matcher.md +++ b/docs/api/matcher.md @@ -7,7 +7,7 @@ sidebarDepth: 0 ## 事件响应器 -该模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行 对话 。 +该模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话 。 ## `matchers` @@ -202,7 +202,7 @@ sidebarDepth: 0 * **类型** - `Optional[T_ArgsParser]` + `Optional[T_TypeUpdater]` @@ -217,7 +217,7 @@ sidebarDepth: 0 * **类型** - `Optional[T_ArgsParser]` + `Optional[T_PermissionUpdater]` @@ -237,7 +237,7 @@ sidebarDepth: 0 * **类型** - `List[T_Handler]` + `List[Handler]` diff --git a/docs/api/plugin.md b/docs/api/plugin.md index a924bb6b..ca0827ce 100644 --- a/docs/api/plugin.md +++ b/docs/api/plugin.md @@ -25,38 +25,6 @@ sidebarDepth: 0 -## _class_ `Export` - -基类:`dict` - - -* **说明** - - 插件导出内容以使得其他插件可以获得。 - - - -* **示例** - - -```python -nonebot.export().default = "bar" - -@nonebot.export() -def some_function(): - pass - -# this doesn't work before python 3.9 -# use -# export = nonebot.export(); @export.sub -# instead -# See also PEP-614: https://www.python.org/dev/peps/pep-0614/ -@nonebot.export().sub -def something_else(): - pass -``` - - ## _class_ `Plugin` 基类:`object` @@ -82,15 +50,6 @@ def something_else(): * **说明**: 插件模块对象 -### `matcher` - - -* **类型**: `Set[Type[Matcher]]` - - -* **说明**: 插件内定义的 `Matcher` - - ### `export` @@ -100,6 +59,15 @@ def something_else(): * **说明**: 插件内定义的导出内容 +### _property_ `matcher` + + +* **类型**: `Set[Type[Matcher]]` + + +* **说明**: 插件内定义的 `Matcher` + + ## `on(type='', rule=None, permission=None, *, handlers=None, temp=False, priority=1, block=False, state=None, state_factory=None)` @@ -121,7 +89,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -162,7 +130,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -206,7 +174,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -247,7 +215,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -288,7 +256,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -335,7 +303,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -382,7 +350,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -429,7 +397,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -481,7 +449,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -538,7 +506,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -590,7 +558,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -767,7 +735,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -808,7 +776,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -852,7 +820,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -893,7 +861,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -934,7 +902,7 @@ def something_else(): * `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -981,7 +949,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1028,7 +996,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1075,7 +1043,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1127,7 +1095,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1184,7 +1152,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1236,7 +1204,7 @@ def something_else(): * `permission: Optional[Permission]`: 事件响应权限 - * `handlers: Optional[List[T_Handler]]`: 事件处理函数列表 + * `handlers: Optional[List[Union[T_Handler, Handler]]]`: 事件处理函数列表 * `temp: bool`: 是否为临时事件响应器(仅执行一次) @@ -1442,22 +1410,6 @@ def something_else(): -## `export()` - - -* **说明** - - 获取插件的导出内容对象 - - - -* **返回** - - - * `Export` - - - ## `require(name)` @@ -1478,3 +1430,51 @@ def something_else(): * `Optional[Export]` + + + +## _class_ `Export` + +基类:`dict` + + +* **说明** + + 插件导出内容以使得其他插件可以获得。 + + + +* **示例** + + +```python +nonebot.export().default = "bar" + +@nonebot.export() +def some_function(): + pass + +# this doesn't work before python 3.9 +# use +# export = nonebot.export(); @export.sub +# instead +# See also PEP-614: https://www.python.org/dev/peps/pep-0614/ +@nonebot.export().sub +def something_else(): + pass +``` + + +## `export()` + + +* **说明** + + 获取插件的导出内容对象 + + + +* **返回** + + + * `Export` diff --git a/docs/guide/ding-guide.md b/docs/guide/ding-guide.md index 7e1d8d01..cb710d26 100644 --- a/docs/guide/ding-guide.md +++ b/docs/guide/ding-guide.md @@ -11,6 +11,10 @@ - [群机器人概述](https://developers.dingtalk.com/document/app/overview-of-group-robots) - [开发企业内部机器人](https://developers.dingtalk.com/document/app/develop-enterprise-internal-robots) +钉钉官方机器人教程(Java): + +- [开发一个钉钉机器人](https://developers.dingtalk.com/document/tutorial/create-a-robot) + ## 安装 NoneBot 钉钉 适配器 ```bash @@ -93,6 +97,58 @@ async def raw_handler(bot: DingBot, event: MessageEvent): 其他消息格式请查看 [钉钉适配器的 MessageSegment](https://github.com/nonebot/nonebot2/blob/dev/nonebot/adapters/ding/message.py#L8),里面封装了很多有关消息的方法,比如 `code`、`image`、`feedCard` 等。 +## 发送到特定群聊 + +钉钉也支持通过 Webhook 的方式直接将消息推送到某个群聊([参考链接](https://developers.dingtalk.com/document/app/custom-robot-access/title-zob-eyu-qse)),你可以在机器人的设置中看到当前群的 Webhook 地址。 + +![机器人所在群的 Webhook 地址](./images/ding/webhook.png) + +获取到Webhook地址后,用户可以向这个地址发起HTTP POST 请求,即可实现给该钉钉群发送消息。 + +对于这种通过 Webhook 推送的消息,钉钉需要开发者进行安全方面的设置(目前有3种安全设置方式,请根据需要选择一种),如下: + +1. **自定义关键词:** 最多可以设置10个关键词,消息中至少包含其中1个关键词才可以发送成功。 + 例如添加了一个自定义关键词:监控报警,则这个机器人所发送的消息,必须包含监控报警这个词,才能发送成功。 +2. **加签:** 发送请求时带上验签的值,可以在机器人设置里看到密钥。 + ![加签密钥](./images/ding/jiaqian.png) +3. **IP地址(段):** 设定后,只有来自IP地址范围内的请求才会被正常处理。支持两种设置方式:IP地址和IP地址段,暂不支持IPv6地址白名单。 + +如果你选择 1/3 两种安全设置,你需要自己确认当前网络和发送的消息能被钉钉接受,然后使用 `bot.send` 的时候将 webhook 地址传入 webhook 参数即可。 + +如我设置了 `打卡` 为关键词: + +```python +message = MessageSegment.text("打卡成功:XXXXXX") +await hello.send( + message, + webhook= + "https://oapi.dingtalk.com/robot/send?access_token=XXXXXXXXXXXXXX", +) +``` + +对于第二种加签方式,你可以在 `bot.send` 的时候把 `secret` 参数传进去,Nonebot 内部会自动帮你计算发送该消息的签名并发送,如: + +这里的 `secret` 参数就是加签选项给出的那个密钥。 + +```python +message = MessageSegment.raw({ + "msgtype": "text", + "text": { + "content": 'hello from webhook,一定要注意安全方式的鉴权哦,否则可能发送失败的' + }, +}) +message += MessageSegment.atDingtalkIds(event.senderId) +await hello.send( + message, + webhook="https://oapi.dingtalk.com/robot/send?access_token=XXXXXXXXXXXXXX", + secret="SECXXXXXXXXXXXXXXXXXXXXXXXXX", +) +``` + +然后就可以发送成功了。 + +![测试 Webhook 发送](images/ding/test_webhook.png) + ## 创建机器人并连接 在钉钉官方文档 [「开发企业内部机器人 -> 步骤一:创建机器人应用」](https://developers.dingtalk.com/document/app/develop-enterprise-internal-robots/title-ufs-4gh-poh) 中有详细介绍,这里就省去创建的步骤,介绍一下如何连接上程序。 diff --git a/docs/guide/end-or-start.md b/docs/guide/end-or-start.md index 9587c4bb..aa072c3d 100644 --- a/docs/guide/end-or-start.md +++ b/docs/guide/end-or-start.md @@ -4,6 +4,5 @@ - 请千万注意事件处理器的优先级设定 - 在匹配规则中请勿使用耗时极长的函数 -- 同一个用户可以**跨群**(**私聊**)继续他的事件处理(除非做出权限限制,将在后续介绍) 如果「指南」还不能满足你,前往 [进阶](../advanced/README.md) 查看更多的功能信息。 diff --git a/docs/guide/images/ding/jiaqian.png b/docs/guide/images/ding/jiaqian.png new file mode 100644 index 00000000..8895d6c6 Binary files /dev/null and b/docs/guide/images/ding/jiaqian.png differ diff --git a/docs/guide/images/ding/test_webhook.png b/docs/guide/images/ding/test_webhook.png new file mode 100644 index 00000000..6620003d Binary files /dev/null and b/docs/guide/images/ding/test_webhook.png differ diff --git a/docs/guide/images/ding/webhook.png b/docs/guide/images/ding/webhook.png new file mode 100644 index 00000000..c957e72f Binary files /dev/null and b/docs/guide/images/ding/webhook.png differ diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 1c6b5d38..04e5e7af 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -68,6 +68,7 @@ pip install . # 不推荐 ``` ## 安装适配器 + 适配器可以通过 `nb-cli` 在创建项目时根据你的选择自动安装,也可以自行使用 `pip` 安装 ```bash @@ -99,6 +100,7 @@ nb plugin install xxx - [NoneBot-Plugin-Docs](https://github.com/nonebot/nonebot2/tree/master/packages/nonebot-plugin-docs) 离线文档插件 - [NoneBot-Plugin-Test](https://github.com/nonebot/plugin-test) 本地机器人测试前端插件 - [NoneBot-Plugin-APScheduler](https://github.com/nonebot/plugin-apscheduler) 定时任务插件 +- [NoneBot-Plugin-LocalStore](https://github.com/nonebot/plugin-localstore) 本地数据文件存储插件 - [NoneBot-Plugin-Sentry](https://github.com/cscs181/QQ-GitHub-Bot/tree/master/src/plugins/nonebot_plugin_sentry) Sentry 在线日志分析插件 - [NoneBot-Plugin-Status](https://github.com/cscs181/QQ-GitHub-Bot/tree/master/src/plugins/nonebot_plugin_status) 服务器状态查看插件 diff --git a/docs_build/README.rst b/docs_build/README.rst index 0e029d9b..745fc781 100644 --- a/docs_build/README.rst +++ b/docs_build/README.rst @@ -7,6 +7,7 @@ NoneBot Api Reference - `nonebot.plugin `_ - `nonebot.message `_ - `nonebot.matcher `_ + - `nonebot.handler `_ - `nonebot.rule `_ - `nonebot.permission `_ - `nonebot.log `_ diff --git a/docs_build/handler.rst b/docs_build/handler.rst new file mode 100644 index 00000000..d7376b97 --- /dev/null +++ b/docs_build/handler.rst @@ -0,0 +1,13 @@ +--- +contentSidebar: true +sidebarDepth: 0 +--- + +NoneBot.handler 模块 +==================== + +.. automodule:: nonebot.handler + :members: + :private-members: + :special-members: __init__ + :show-inheritance: diff --git a/docs_build/plugin.rst b/docs_build/plugin.rst index 752fbe4a..9c463240 100644 --- a/docs_build/plugin.rst +++ b/docs_build/plugin.rst @@ -10,3 +10,8 @@ NoneBot.plugin 模块 :members: :show-inheritance: :special-members: __init__ + +.. automodule:: nonebot.plugin.export + :members: + :show-inheritance: + :special-members: __init__ diff --git a/nonebot/adapters/_base.py b/nonebot/adapters/_base.py index 5983838a..264f69c7 100644 --- a/nonebot/adapters/_base.py +++ b/nonebot/adapters/_base.py @@ -6,21 +6,30 @@ """ import abc +import asyncio from copy import copy -from typing_extensions import Literal from functools import reduce, partial from dataclasses import dataclass, field -from typing import Any, Dict, Union, TypeVar, Mapping, Optional, Callable, Iterable, Iterator, Awaitable, TYPE_CHECKING +from typing import (Any, Set, Dict, Union, TypeVar, Mapping, Optional, Iterable, + Protocol, Awaitable, TYPE_CHECKING) from pydantic import BaseModel +from nonebot.log import logger from nonebot.utils import DataclassEncoder +from nonebot.typing import T_CallingAPIHook if TYPE_CHECKING: from nonebot.config import Config from nonebot.drivers import Driver, WebSocket +class _ApiCall(Protocol): + + def __call__(self, **kwargs: Any) -> Awaitable[Any]: + ... + + class Bot(abc.ABC): """ Bot 基类。用于处理上报消息,并提供 API 调用接口。 @@ -30,6 +39,11 @@ class Bot(abc.ABC): """Driver 对象""" config: "Config" """Config 配置对象""" + _call_api_hook: Set[T_CallingAPIHook] = set() + """ + :类型: ``Set[T_CallingAPIHook]`` + :说明: call_api 时执行的函数 + """ @abc.abstractmethod def __init__(self, @@ -51,7 +65,7 @@ class Bot(abc.ABC): self.websocket = websocket """Websocket 连接对象""" - def __getattr__(self, name: str) -> Callable[..., Awaitable[Any]]: + def __getattr__(self, name: str) -> _ApiCall: return partial(self.call_api, name) @property @@ -73,7 +87,7 @@ class Bot(abc.ABC): @classmethod @abc.abstractmethod async def check_permission(cls, driver: "Driver", connection_type: str, - headers: dict, body: Optional[dict]) -> str: + headers: dict, body: Optional[bytes]) -> str: """ :说明: @@ -84,7 +98,7 @@ class Bot(abc.ABC): * ``driver: Driver``: Driver 对象 * ``connection_type: str``: 连接类型 * ``headers: dict``: 请求头 - * ``body: Optional[dict]``: 请求数据,WebSocket 连接该部分为空 + * ``body: Optional[bytes]``: 请求数据,WebSocket 连接该部分为 None :返回: @@ -110,7 +124,20 @@ class Bot(abc.ABC): raise NotImplementedError @abc.abstractmethod - async def call_api(self, api: str, **data): + async def _call_api(self, api: str, **data) -> Any: + """ + :说明: + + ``adapter`` 实际调用 api 的逻辑实现函数,实现该方法以调用 api。 + + :参数: + + * ``api: str``: API 名称 + * ``**data``: API 数据 + """ + raise NotImplementedError + + async def call_api(self, api: str, **data: Any) -> Any: """ :说明: @@ -119,6 +146,7 @@ class Bot(abc.ABC): :参数: * ``api: str``: API 名称 + * ``self_id: Optional[str]``: 指定调用 API 的机器人 * ``**data``: API 数据 :示例: @@ -128,11 +156,28 @@ class Bot(abc.ABC): await bot.call_api("send_msg", message="hello world") await bot.send_msg(message="hello world") """ - raise NotImplementedError + coros = list(map(lambda x: x(self, api, data), self._call_api_hook)) + if coros: + try: + logger.debug("Running CallingAPI hooks...") + await asyncio.gather(*coros) + except Exception as e: + logger.opt(colors=True, exception=e).error( + "Error when running CallingAPI hook. " + "Running cancelled!") + + if "self_id" in data: + self_id = data.pop("self_id") + if self_id: + bot = self.driver.bots[str(self_id)] + return await bot._call_api(api, **data) + + return await self._call_api(api, **data) @abc.abstractmethod - async def send(self, event: "Event", - message: Union[str, "Message", "MessageSegment"], **kwargs): + async def send(self, event: "Event", message: Union[str, "Message", + "MessageSegment"], + **kwargs) -> Any: """ :说明: @@ -146,6 +191,11 @@ class Bot(abc.ABC): """ raise NotImplementedError + @classmethod + def on_calling_api(cls, func: T_CallingAPIHook) -> T_CallingAPIHook: + cls._call_api_hook.add(func) + return func + T_Message = TypeVar("T_Message", bound="Message") T_MessageSegment = TypeVar("T_MessageSegment", bound="MessageSegment") diff --git a/nonebot/drivers/fastapi.py b/nonebot/drivers/fastapi.py index a2c315f5..18a5f1db 100644 --- a/nonebot/drivers/fastapi.py +++ b/nonebot/drivers/fastapi.py @@ -16,7 +16,7 @@ from typing import List, Optional, Callable import uvicorn from pydantic import BaseSettings from fastapi.responses import Response -from fastapi import Body, status, Request, FastAPI, HTTPException +from fastapi import status, Request, FastAPI, HTTPException from starlette.websockets import WebSocketDisconnect, WebSocket as FastAPIWebSocket from nonebot.log import logger @@ -177,11 +177,11 @@ class Driver(BaseDriver): **kwargs) @overrides(BaseDriver) - async def _handle_http(self, - adapter: str, - request: Request, - data: dict = Body(...)): - if not isinstance(data, dict): + async def _handle_http(self, adapter: str, request: Request): + data = await request.body() + data_dict = json.loads(data.decode()) + + if not isinstance(data_dict, dict): logger.warning("Data received is invalid") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) @@ -208,7 +208,7 @@ class Driver(BaseDriver): bot = BotClass("http", x_self_id) - asyncio.create_task(bot.handle_message(data)) + asyncio.create_task(bot.handle_message(data_dict)) return Response("", 204) @overrides(BaseDriver) @@ -234,8 +234,9 @@ class Driver(BaseDriver): return if x_self_id in self._clients: - logger.warning("There's already a reverse websocket connection, " - f"{adapter.upper()} Bot {x_self_id} ignored.") + logger.opt(colors=True).warning( + "There's already a reverse websocket connection, " + f"{adapter.upper()} Bot {x_self_id} ignored.") await ws.close(code=status.WS_1008_POLICY_VIOLATION) return diff --git a/nonebot/handler.py b/nonebot/handler.py new file mode 100644 index 00000000..acc35165 --- /dev/null +++ b/nonebot/handler.py @@ -0,0 +1,193 @@ +""" +事件处理函数 +=========== + +该模块实现事件处理函数的封装,以实现动态参数等功能。 +""" + +import inspect +from typing import Any, List, Dict, Type, Union, Optional, TYPE_CHECKING +from typing import ForwardRef, _eval_type # type: ignore + +from nonebot.log import logger +from nonebot.typing import T_Handler, T_State + +if TYPE_CHECKING: + from nonebot.matcher import Matcher + from nonebot.adapters import Bot, Event + + +class HandlerMeta(type): + if TYPE_CHECKING: + func: T_Handler + signature: inspect.Signature + bot_type: Type["Bot"] + event_type: Optional[Type["Event"]] + state_type: Optional[T_State] + matcher_type: Optional[Type["Matcher"]] + + def __repr__(self) -> str: + return (f"") + + def __str__(self) -> str: + return repr(self) + + +class Handler(metaclass=HandlerMeta): + """事件处理函数类""" + + def __init__(self, func: T_Handler): + """装饰事件处理函数以便根据动态参数运行""" + self.func: T_Handler = func + """ + :类型: ``T_Handler`` + :说明: 事件处理函数 + """ + self.signature: inspect.Signature = self.get_signature() + """ + :类型: ``inspect.Signature`` + :说明: 事件处理函数签名 + """ + + def __repr__(self) -> str: + return (f"") + + def __str__(self) -> str: + return repr(self) + + async def __call__(self, matcher: "Matcher", bot: "Bot", event: "Event", + state: T_State): + BotType = ((self.bot_type is not inspect.Parameter.empty) and + inspect.isclass(self.bot_type) and self.bot_type) + if BotType and not isinstance(bot, BotType): + logger.debug( + f"Matcher {matcher} bot type {type(bot)} not match annotation {BotType}, ignored" + ) + return + + EventType = ((self.event_type is not inspect.Parameter.empty) and + inspect.isclass(self.event_type) and self.event_type) + if EventType and not isinstance(event, EventType): + logger.debug( + f"Matcher {matcher} event type {type(event)} not match annotation {EventType}, ignored" + ) + return + + args = {"bot": bot, "event": event, "state": state, "matcher": matcher} + await self.func( + **{ + k: v + for k, v in args.items() + if self.signature.parameters.get(k, None) is not None + }) + + @property + def bot_type(self) -> Union[Type["Bot"], inspect.Parameter.empty]: + """ + :类型: ``Union[Type["Bot"], inspect.Parameter.empty]`` + :说明: 事件处理函数接受的 Bot 对象类型""" + return self.signature.parameters["bot"].annotation + + @property + def event_type( + self) -> Optional[Union[Type["Event"], inspect.Parameter.empty]]: + """ + :类型: ``Optional[Union[Type[Event], inspect.Parameter.empty]]`` + :说明: 事件处理函数接受的 event 类型 / 不需要 event 参数 + """ + if "event" not in self.signature.parameters: + return None + return self.signature.parameters["event"].annotation + + @property + def state_type(self) -> Optional[Union[T_State, inspect.Parameter.empty]]: + """ + :类型: ``Optional[Union[T_State, inspect.Parameter.empty]]`` + :说明: 事件处理函数是否接受 state 参数 + """ + if "state" not in self.signature.parameters: + return None + return self.signature.parameters["state"].annotation + + @property + def matcher_type( + self) -> Optional[Union[Type["Matcher"], inspect.Parameter.empty]]: + """ + :类型: ``Optional[Union[Type["Matcher"], inspect.Parameter.empty]]`` + :说明: 事件处理函数是否接受 matcher 参数 + """ + if "matcher" not in self.signature.parameters: + return None + return self.signature.parameters["matcher"].annotation + + def get_signature(self) -> inspect.Signature: + wrapped_signature = self._get_typed_signature() + signature = self._get_typed_signature(False) + self._check_params(signature) + self._check_bot_param(signature) + self._check_bot_param(wrapped_signature) + signature.parameters["bot"].replace( + annotation=wrapped_signature.parameters["bot"].annotation) + if "event" in wrapped_signature.parameters and "event" in signature.parameters: + signature.parameters["event"].replace( + annotation=wrapped_signature.parameters["event"].annotation) + return signature + + def update_signature( + self, **kwargs: Union[None, Type["Bot"], Type["Event"], Type["Matcher"], + T_State, inspect.Parameter.empty] + ) -> None: + params: List[inspect.Parameter] = [] + for param in ["bot", "event", "state", "matcher"]: + sig = self.signature.parameters.get(param, None) + if param in kwargs: + sig = inspect.Parameter(param, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + annotation=kwargs[param]) + if sig: + params.append(sig) + + self.signature = inspect.Signature(params) + + def _get_typed_signature(self, + follow_wrapped: bool = True) -> inspect.Signature: + signature = inspect.signature(self.func, follow_wrapped=follow_wrapped) + globalns = getattr(self.func, "__globals__", {}) + typed_params = [ + inspect.Parameter( + name=param.name, + kind=param.kind, + default=param.default, + annotation=param.annotation if follow_wrapped else + self._get_typed_annotation(param, globalns), + ) for param in signature.parameters.values() + ] + typed_signature = inspect.Signature(typed_params) + return typed_signature + + def _get_typed_annotation(self, param: inspect.Parameter, + globalns: Dict[str, Any]) -> Any: + try: + if isinstance(param.annotation, str): + return _eval_type(ForwardRef(param.annotation), globalns, + globalns) + else: + return param.annotation + except Exception: + return param.annotation + + def _check_params(self, signature: inspect.Signature): + if not set(signature.parameters.keys()) <= { + "bot", "event", "state", "matcher" + }: + raise ValueError( + "Handler param names must in `bot`/`event`/`state`/`matcher`") + + def _check_bot_param(self, signature: inspect.Signature): + if not any( + param.name == "bot" for param in signature.parameters.values()): + raise ValueError("Handler missing parameter 'bot'") diff --git a/nonebot/log.py b/nonebot/log.py index 854b4b78..3f6d1e1a 100644 --- a/nonebot/log.py +++ b/nonebot/log.py @@ -77,8 +77,8 @@ default_format = ( "{name} | " # "{function}:{line}| " "{message}") -logger.add(sys.stdout, - colorize=True, - diagnose=False, - filter=default_filter, - format=default_format) +logger_id = logger.add(sys.stdout, + colorize=True, + diagnose=False, + filter=default_filter, + format=default_format) diff --git a/nonebot/matcher.py b/nonebot/matcher.py index 942d4a2c..771fcc65 100644 --- a/nonebot/matcher.py +++ b/nonebot/matcher.py @@ -2,21 +2,24 @@ 事件响应器 ========== -该模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行 对话 。 +该模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话 。 """ -import inspect from functools import wraps from datetime import datetime from contextvars import ContextVar from collections import defaultdict -from typing import Type, List, Dict, Union, Mapping, Iterable, Callable, Optional, NoReturn, TYPE_CHECKING +from typing import (Any, Type, List, Dict, Union, Mapping, Iterable, Callable, + Optional, NoReturn, TYPE_CHECKING) from nonebot.rule import Rule from nonebot.log import logger +from nonebot.handler import Handler from nonebot.permission import Permission, USER -from nonebot.typing import T_State, T_StateFactory, T_Handler, T_ArgsParser, T_TypeUpdater, T_PermissionUpdater -from nonebot.exception import PausedException, RejectedException, FinishedException, StopPropagation +from nonebot.typing import (T_State, T_StateFactory, T_Handler, T_ArgsParser, + T_TypeUpdater, T_PermissionUpdater) +from nonebot.exception import (PausedException, RejectedException, + FinishedException, StopPropagation) if TYPE_CHECKING: from nonebot.adapters import Bot, Event, Message, MessageSegment @@ -31,11 +34,21 @@ current_event: ContextVar = ContextVar("current_event") class MatcherMeta(type): + if TYPE_CHECKING: + module: 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"") # type: ignore + return (f"") def __str__(self) -> str: return repr(self) @@ -64,9 +77,9 @@ class Matcher(metaclass=MatcherMeta): :类型: ``Permission`` :说明: 事件响应器触发权限 """ - handlers: List[T_Handler] = [] + handlers: List[Handler] = [] """ - :类型: ``List[T_Handler]`` + :类型: ``List[Handler]`` :说明: 事件响应器拥有的事件处理函数列表 """ priority: int = 1 @@ -108,12 +121,12 @@ class Matcher(metaclass=MatcherMeta): """ _default_type_updater: Optional[T_TypeUpdater] = None """ - :类型: ``Optional[T_ArgsParser]`` + :类型: ``Optional[T_TypeUpdater]`` :说明: 事件响应器类型更新函数 """ _default_permission_updater: Optional[T_PermissionUpdater] = None """ - :类型: ``Optional[T_ArgsParser]`` + :类型: ``Optional[T_PermissionUpdater]`` :说明: 事件响应器权限更新函数 """ @@ -127,14 +140,15 @@ class Matcher(metaclass=MatcherMeta): f"priority={self.priority}, temp={self.temp}>") def __str__(self) -> str: - return self.__repr__() + return repr(self) @classmethod def new(cls, type_: str = "", rule: Optional[Rule] = None, permission: Optional[Permission] = None, - handlers: Optional[List[T_Handler]] = None, + handlers: Optional[Union[List[T_Handler], List[Handler], + List[Union[T_Handler, Handler]]]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -178,7 +192,9 @@ class Matcher(metaclass=MatcherMeta): "permission": permission or Permission(), "handlers": [ - cls.process_handler(handler) for handler in handlers + handler + if isinstance(handler, Handler) else Handler(handler) + for handler in handlers ] if handlers else [], "temp": temp, @@ -284,27 +300,11 @@ class Matcher(metaclass=MatcherMeta): cls._default_permission_updater = func return func - @staticmethod - def process_handler(handler: T_Handler) -> T_Handler: - signature = inspect.signature(handler, follow_wrapped=False) - bot = signature.parameters.get("bot") - event = signature.parameters.get("event") - state = signature.parameters.get("state") - matcher = signature.parameters.get("matcher") - if not bot: - raise ValueError("Handler missing parameter 'bot'") - handler.__params__ = { - "bot": bot.annotation, - "event": event.annotation if event else None, - "state": T_State if state else None, - "matcher": matcher.annotation if matcher else None - } - return handler - @classmethod - def append_handler(cls, handler: T_Handler) -> None: - # Process handler first - cls.handlers.append(cls.process_handler(handler)) + def append_handler(cls, handler: T_Handler) -> Handler: + handler_ = Handler(handler) + cls.handlers.append(handler_) + return handler_ @classmethod def handle(cls) -> Callable[[T_Handler], T_Handler]: @@ -339,23 +339,19 @@ class Matcher(metaclass=MatcherMeta): async def _receive(bot: "Bot", event: "Event") -> NoReturn: raise PausedException - cls.process_handler(_receive) - if cls.handlers: # 已有前置handlers则接受一条新的消息,否则视为接收初始消息 - cls.append_handler(_receive) + receive_handler = cls.append_handler(_receive) + else: + receive_handler = None def _decorator(func: T_Handler) -> T_Handler: - cls.process_handler(func) if not cls.handlers or cls.handlers[-1] is not func: - cls.append_handler(func) - - _receive.__params__.update({ - "bot": - func.__params__["bot"], - "event": - func.__params__["event"] or _receive.__params__["event"] - }) + func_handler = cls.append_handler(func) + if receive_handler: + receive_handler.update_signature( + bot=func_handler.bot_type, + event=func_handler.event_type) return func @@ -416,42 +412,27 @@ class Matcher(metaclass=MatcherMeta): else: state[state["_current_key"]] = str(event.get_message()) - cls.append_handler(_key_getter) - cls.append_handler(_key_parser) + getter_handler = cls.append_handler(_key_getter) + parser_handler = cls.append_handler(_key_parser) def _decorator(func: T_Handler) -> T_Handler: if not hasattr(cls.handlers[-1], "__wrapped__"): - cls.process_handler(func) parser = cls.handlers.pop() @wraps(func) async def wrapper(bot: "Bot", event: "Event", state: T_State, matcher: Matcher): - await matcher.run_handler(parser, bot, event, state) - await matcher.run_handler(func, bot, event, state) + await parser(matcher, bot, event, state) + await func_handler(matcher, bot, event, state) if "_current_key" in state: del state["_current_key"] - cls.append_handler(wrapper) + func_handler = cls.append_handler(wrapper) - wrapper.__params__.update({ - "bot": - func.__params__["bot"], - "event": - func.__params__["event"] or wrapper.__params__["event"] - }) - _key_getter.__params__.update({ - "bot": - func.__params__["bot"], - "event": - func.__params__["event"] or wrapper.__params__["event"] - }) - _key_parser.__params__.update({ - "bot": - func.__params__["bot"], - "event": - func.__params__["event"] or wrapper.__params__["event"] - }) + getter_handler.update_signature(bot=func_handler.bot_type, + event=func_handler.event_type) + parser_handler.update_signature(bot=func_handler.bot_type, + event=func_handler.event_type) return func @@ -459,7 +440,7 @@ class Matcher(metaclass=MatcherMeta): @classmethod async def send(cls, message: Union[str, "Message", "MessageSegment"], - **kwargs): + **kwargs) -> Any: """ :说明: @@ -470,9 +451,9 @@ class Matcher(metaclass=MatcherMeta): * ``message: Union[str, Message, MessageSegment]``: 消息内容 * ``**kwargs``: 其他传递给 ``bot.send`` 的参数,请参考对应 adapter 的 bot 对象 api """ - bot = current_bot.get() + bot: "Bot" = current_bot.get() event = current_event.get() - await bot.send(event=event, message=message, **kwargs) + return await bot.send(event=event, message=message, **kwargs) @classmethod async def finish(cls, @@ -545,32 +526,6 @@ class Matcher(metaclass=MatcherMeta): """ self.block = True - async def run_handler(self, handler: T_Handler, bot: "Bot", event: "Event", - state: T_State): - if not hasattr(handler, "__params__"): - self.process_handler(handler) - params = getattr(handler, "__params__") - - BotType = ((params["bot"] is not inspect.Parameter.empty) and - inspect.isclass(params["bot"]) and params["bot"]) - if BotType and not isinstance(bot, BotType): - logger.debug( - f"Matcher {self} bot type {type(bot)} not match annotation {BotType}, ignored" - ) - return - - EventType = ((params["event"] is not inspect.Parameter.empty) and - inspect.isclass(params["event"]) and params["event"]) - if EventType and not isinstance(event, EventType): - logger.debug( - f"Matcher {self} event type {type(event)} not match annotation {EventType}, ignored" - ) - return - - args = {"bot": bot, "event": event, "state": state, "matcher": self} - await handler( - **{k: v for k, v in args.items() if params[k] is not None}) - # 运行handlers async def run(self, bot: "Bot", event: "Event", state: T_State): b_t = current_bot.set(bot) @@ -583,20 +538,22 @@ class Matcher(metaclass=MatcherMeta): for _ in range(len(self.handlers)): handler = self.handlers.pop(0) - await self.run_handler(handler, bot, event, state_) + await handler(self, bot, event, state_) except RejectedException: self.handlers.insert(0, handler) # type: ignore - if self._default_type_updater: - type_ = await self._default_type_updater( - bot, event, state, self.type) + updater = self.__class__._default_type_updater + if updater: + type_ = await updater(bot, event, state, self.type) else: type_ = "message" - if self._default_permission_updater: - permission = await self._default_permission_updater( - bot, event, state, self.permission) + + updater = self.__class__._default_permission_updater + if updater: + permission = await updater(bot, event, state, self.permission) else: permission = USER(event.get_session_id(), perm=self.permission) + Matcher.new(type_, Rule(), permission, @@ -609,16 +566,18 @@ class Matcher(metaclass=MatcherMeta): expire_time=datetime.now() + bot.config.session_expire_timeout) except PausedException: - if self._default_type_updater: - type_ = await self._default_type_updater( - bot, event, state, self.type) + updater = self.__class__._default_type_updater + if updater: + type_ = await updater(bot, event, state, self.type) else: type_ = "message" - if self._default_permission_updater: - permission = await self._default_permission_updater( - bot, event, state, self.permission) + + updater = self.__class__._default_permission_updater + if updater: + permission = await updater(bot, event, state, self.permission) else: permission = USER(event.get_session_id(), perm=self.permission) + Matcher.new(type_, Rule(), permission, diff --git a/nonebot/message.py b/nonebot/message.py index cc226875..469b1df2 100644 --- a/nonebot/message.py +++ b/nonebot/message.py @@ -205,7 +205,8 @@ async def handle_event(bot: "Bot", event: "Event"): coros = list(map(lambda x: x(bot, event, state), _event_preprocessors)) if coros: try: - logger.debug("Running PreProcessors...") + if show_log: + logger.debug("Running PreProcessors...") await asyncio.gather(*coros) except IgnoredException: logger.opt(colors=True).info( @@ -240,11 +241,16 @@ async def handle_event(bot: "Bot", event: "Event"): if not break_flag: break_flag = True logger.debug("Stop event propagation") + elif isinstance(result, Exception): + logger.opt(colors=True, exception=result).error( + "Error when checking Matcher." + ) coros = list(map(lambda x: x(bot, event, state), _event_postprocessors)) if coros: try: - logger.debug("Running PostProcessors...") + if show_log: + logger.debug("Running PostProcessors...") await asyncio.gather(*coros) except Exception as e: logger.opt(colors=True, exception=e).error( diff --git a/nonebot/plugin/__init__.py b/nonebot/plugin/__init__.py index b9ed77ed..f63bb97b 100644 --- a/nonebot/plugin/__init__.py +++ b/nonebot/plugin/__init__.py @@ -8,17 +8,20 @@ import re import json from types import ModuleType from dataclasses import dataclass +from collections import defaultdict from contextvars import Context, ContextVar, copy_context from typing import Any, Set, List, Dict, Type, Tuple, Union, Optional, TYPE_CHECKING import tomlkit from nonebot.log import logger from nonebot.matcher import Matcher +from nonebot.handler import Handler from nonebot.permission import Permission from nonebot.typing import T_State, T_StateFactory, T_Handler, T_RuleChecker from nonebot.rule import Rule, startswith, endswith, keyword, command, shell_command, ArgumentParser, regex -from .manager import PluginManager +from .export import Export, export, _export +from .manager import PluginManager, _current_plugin if TYPE_CHECKING: from nonebot.adapters import Bot, Event @@ -30,52 +33,7 @@ plugins: Dict[str, "Plugin"] = {} """ PLUGIN_NAMESPACE = "nonebot.loaded_plugins" -_export: ContextVar["Export"] = ContextVar("_export") -_tmp_matchers: ContextVar[Set[Type[Matcher]]] = ContextVar("_tmp_matchers") - - -class Export(dict): - """ - :说明: - - 插件导出内容以使得其他插件可以获得。 - - :示例: - - .. code-block:: python - - nonebot.export().default = "bar" - - @nonebot.export() - def some_function(): - pass - - # this doesn't work before python 3.9 - # use - # export = nonebot.export(); @export.sub - # instead - # See also PEP-614: https://www.python.org/dev/peps/pep-0614/ - @nonebot.export().sub - def something_else(): - pass - """ - - def __call__(self, func, **kwargs): - self[func.__name__] = func - self.update(kwargs) - return func - - def __setitem__(self, key, value): - super().__setitem__(key, - Export(value) if isinstance(value, dict) else value) - - def __setattr__(self, name, value): - self[name] = Export(value) if isinstance(value, dict) else value - - def __getattr__(self, name): - if name not in self: - self[name] = Export() - return self[name] +_plugin_matchers: Dict[str, Set[Type[Matcher]]] = defaultdict(set) @dataclass(eq=False) @@ -91,23 +49,31 @@ class Plugin(object): - **类型**: ``ModuleType`` - **说明**: 插件模块对象 """ - matcher: Set[Type[Matcher]] - """ - - **类型**: ``Set[Type[Matcher]]`` - - **说明**: 插件内定义的 ``Matcher`` - """ export: Export """ - **类型**: ``Export`` - **说明**: 插件内定义的导出内容 """ + @property + def matcher(self) -> Set[Type[Matcher]]: + """ + - **类型**: ``Set[Type[Matcher]]`` + - **说明**: 插件内定义的 ``Matcher`` + """ + return _plugin_matchers[self.name] + + +def _store_matcher(matcher: Type[Matcher]): + plugin_name = matcher.module.split(".", maxsplit=1)[0] + _plugin_matchers[plugin_name].add(matcher) + def on(type: str = "", rule: Optional[Union[Rule, T_RuleChecker]] = None, permission: Optional[Permission] = None, *, - handlers: Optional[List[T_Handler]] = None, + handlers: Optional[List[Union[T_Handler, Handler]]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -123,7 +89,7 @@ def on(type: str = "", * ``type: str``: 事件响应器类型 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -141,16 +107,17 @@ def on(type: str = "", priority=priority, block=block, handlers=handlers, + module=_current_plugin.get(), default_state=state, default_state_factory=state_factory) - _tmp_matchers.get().add(matcher) + _store_matcher(matcher) return matcher def on_metaevent( rule: Optional[Union[Rule, T_RuleChecker]] = None, *, - handlers: Optional[List[T_Handler]] = None, + handlers: Optional[List[Union[T_Handler, Handler]]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -164,7 +131,7 @@ def on_metaevent( :参数: * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -182,16 +149,17 @@ def on_metaevent( priority=priority, block=block, handlers=handlers, + module=_current_plugin.get(), default_state=state, default_state_factory=state_factory) - _tmp_matchers.get().add(matcher) + _store_matcher(matcher) return matcher def on_message(rule: Optional[Union[Rule, T_RuleChecker]] = None, permission: Optional[Permission] = None, *, - handlers: Optional[List[T_Handler]] = None, + handlers: Optional[List[Union[T_Handler, Handler]]] = None, temp: bool = False, priority: int = 1, block: bool = True, @@ -206,7 +174,7 @@ def on_message(rule: Optional[Union[Rule, T_RuleChecker]] = None, * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -224,15 +192,16 @@ def on_message(rule: Optional[Union[Rule, T_RuleChecker]] = None, priority=priority, block=block, handlers=handlers, + module=_current_plugin.get(), default_state=state, default_state_factory=state_factory) - _tmp_matchers.get().add(matcher) + _store_matcher(matcher) return matcher def on_notice(rule: Optional[Union[Rule, T_RuleChecker]] = None, *, - handlers: Optional[List[T_Handler]] = None, + handlers: Optional[List[Union[T_Handler, Handler]]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -246,7 +215,7 @@ def on_notice(rule: Optional[Union[Rule, T_RuleChecker]] = None, :参数: * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -264,15 +233,16 @@ def on_notice(rule: Optional[Union[Rule, T_RuleChecker]] = None, priority=priority, block=block, handlers=handlers, + module=_current_plugin.get(), default_state=state, default_state_factory=state_factory) - _tmp_matchers.get().add(matcher) + _store_matcher(matcher) return matcher def on_request(rule: Optional[Union[Rule, T_RuleChecker]] = None, *, - handlers: Optional[List[T_Handler]] = None, + handlers: Optional[List[Union[T_Handler, Handler]]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -286,7 +256,7 @@ def on_request(rule: Optional[Union[Rule, T_RuleChecker]] = None, :参数: * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -304,9 +274,10 @@ def on_request(rule: Optional[Union[Rule, T_RuleChecker]] = None, priority=priority, block=block, handlers=handlers, + module=_current_plugin.get(), default_state=state, default_state_factory=state_factory) - _tmp_matchers.get().add(matcher) + _store_matcher(matcher) return matcher @@ -323,7 +294,7 @@ def on_startswith(msg: str, * ``msg: str``: 指定消息开头内容 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -350,7 +321,7 @@ def on_endswith(msg: str, * ``msg: str``: 指定消息结尾内容 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -377,7 +348,7 @@ def on_keyword(keywords: Set[str], * ``keywords: Set[str]``: 关键词列表 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -408,7 +379,7 @@ def on_command(cmd: Union[str, Tuple[str, ...]], * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``aliases: Optional[Set[Union[str, Tuple[str, ...]]]]``: 命令别名 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -457,7 +428,7 @@ def on_shell_command(cmd: Union[str, Tuple[str, ...]], * ``aliases: Optional[Set[Union[str, Tuple[str, ...]]]]``: 命令别名 * ``parser: Optional[ArgumentParser]``: ``nonebot.rule.ArgumentParser`` 对象 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -489,7 +460,7 @@ def on_shell_command(cmd: Union[str, Tuple[str, ...]], def on_regex(pattern: str, flags: Union[int, re.RegexFlag] = 0, - rule: Optional[Rule] = None, + rule: Optional[Union[Rule, T_RuleChecker]] = None, **kwargs) -> Type[Matcher]: """ :说明: @@ -504,7 +475,7 @@ def on_regex(pattern: str, * ``flags: Union[int, re.RegexFlag]``: 正则匹配标志 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -619,7 +590,7 @@ class MatcherGroup: * ``type: str``: 事件响应器类型 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -645,7 +616,7 @@ class MatcherGroup: :参数: * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -673,7 +644,7 @@ class MatcherGroup: * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -700,7 +671,7 @@ class MatcherGroup: :参数: * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -727,7 +698,7 @@ class MatcherGroup: :参数: * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -756,7 +727,7 @@ class MatcherGroup: * ``msg: str``: 指定消息开头内容 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -785,7 +756,7 @@ class MatcherGroup: * ``msg: str``: 指定消息结尾内容 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -814,7 +785,7 @@ class MatcherGroup: * ``keywords: Set[str]``: 关键词列表 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -849,7 +820,7 @@ class MatcherGroup: * ``aliases: Optional[Set[Union[str, Tuple[str, ...]]]]``: 命令别名 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -889,7 +860,7 @@ class MatcherGroup: * ``parser: Optional[ArgumentParser]``: ``nonebot.rule.ArgumentParser`` 对象 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -927,7 +898,7 @@ class MatcherGroup: * ``flags: Union[int, re.RegexFlag]``: 正则匹配标志 * ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则 * ``permission: Optional[Permission]``: 事件响应权限 - * ``handlers: Optional[List[T_Handler]]``: 事件处理函数列表 + * ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表 * ``temp: bool``: 是否为临时事件响应器(仅执行一次) * ``priority: int``: 事件响应器优先级 * ``block: bool``: 是否阻止事件向更低优先级传递 @@ -950,18 +921,14 @@ def _load_plugin(manager: PluginManager, plugin_name: str) -> Optional[Plugin]: if plugin_name.startswith("_"): return None - _tmp_matchers.set(set()) - _export.set(Export()) - if plugin_name in plugins: return None try: module = manager.load_plugin(plugin_name) - for m in _tmp_matchers.get(): - m.module = plugin_name - plugin = Plugin(plugin_name, module, _tmp_matchers.get(), _export.get()) + plugin = Plugin(plugin_name, module, + getattr(module, "__export__", Export())) plugins[plugin_name] = plugin logger.opt( colors=True).info(f'Succeeded to import "{plugin_name}"') @@ -1032,39 +999,11 @@ def load_all_plugins(module_path: Set[str], - ``Set[Plugin]`` """ - - def _load_plugin(plugin_name: str) -> Optional[Plugin]: - if plugin_name.startswith("_"): - return None - - _tmp_matchers.set(set()) - _export.set(Export()) - - if plugin_name in plugins: - return None - - try: - module = manager.load_plugin(plugin_name) - - for m in _tmp_matchers.get(): - m.module = plugin_name - plugin = Plugin(plugin_name, module, _tmp_matchers.get(), - _export.get()) - plugins[plugin_name] = plugin - logger.opt( - colors=True).info(f'Succeeded to import "{plugin_name}"') - return plugin - except Exception as e: - logger.opt(colors=True, exception=e).error( - f'Failed to import "{plugin_name}"' - ) - return None - loaded_plugins = set() manager = PluginManager(PLUGIN_NAMESPACE, module_path, plugin_dir) for plugin_name in manager.list_plugins(): context: Context = copy_context() - result = context.run(_load_plugin, plugin_name) + result = context.run(_load_plugin, manager, plugin_name) if result: loaded_plugins.add(result) return loaded_plugins @@ -1168,19 +1107,6 @@ def get_loaded_plugins() -> Set[Plugin]: return set(plugins.values()) -def export() -> Export: - """ - :说明: - - 获取插件的导出内容对象 - - :返回: - - - ``Export`` - """ - return _export.get() - - def require(name: str) -> Optional[Export]: """ :说明: diff --git a/nonebot/plugin/__init__.pyi b/nonebot/plugin/__init__.pyi index 02cc6b96..bda05b77 100644 --- a/nonebot/plugin/__init__.pyi +++ b/nonebot/plugin/__init__.pyi @@ -4,6 +4,7 @@ from contextvars import ContextVar from typing import Any, Set, List, Dict, Type, Tuple, Union, Optional from nonebot.matcher import Matcher +from nonebot.handler import Handler from nonebot.permission import Permission from nonebot.rule import Rule, ArgumentParser from nonebot.typing import T_State, T_StateFactory, T_Handler, T_RuleChecker @@ -37,7 +38,7 @@ def on(type: str = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ..., permission: Optional[Permission] = ..., *, - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -49,7 +50,7 @@ def on(type: str = ..., def on_metaevent( rule: Optional[Union[Rule, T_RuleChecker]] = ..., *, - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -61,7 +62,7 @@ def on_metaevent( def on_message(rule: Optional[Union[Rule, T_RuleChecker]] = ..., permission: Optional[Permission] = ..., *, - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -72,7 +73,7 @@ def on_message(rule: Optional[Union[Rule, T_RuleChecker]] = ..., def on_notice(rule: Optional[Union[Rule, T_RuleChecker]] = ..., *, - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -83,7 +84,7 @@ def on_notice(rule: Optional[Union[Rule, T_RuleChecker]] = ..., def on_request(rule: Optional[Union[Rule, T_RuleChecker]] = ..., *, - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -97,7 +98,7 @@ def on_startswith( rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ..., *, permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -110,7 +111,7 @@ def on_endswith(msg: str, rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ..., *, permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -123,7 +124,7 @@ def on_keyword(keywords: Set[str], rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ..., *, permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -137,7 +138,7 @@ def on_command(cmd: Union[str, Tuple[str, ...]], aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., *, permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -156,10 +157,10 @@ def on_shell_command(cmd: Union[str, Tuple[str, ...]], def on_regex(pattern: str, flags: Union[int, re.RegexFlag] = 0, - rule: Optional[Rule] = ..., + rule: Optional[Union[Rule, T_RuleChecker]] = ..., *, permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -216,7 +217,7 @@ class CommandGroup: rule: Optional[Union[Rule, T_RuleChecker]] = ..., permission: Optional[Permission] = ..., *, - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -230,7 +231,7 @@ class CommandGroup: rule: Optional[Union[Rule, T_RuleChecker]] = ..., aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -246,7 +247,7 @@ class CommandGroup: aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ..., parser: Optional[ArgumentParser] = ..., permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -262,7 +263,7 @@ class MatcherGroup: type: str = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ..., permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -274,7 +275,7 @@ class MatcherGroup: type: str = ..., rule: Optional[Union[Rule, T_RuleChecker]] = ..., permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -286,7 +287,7 @@ class MatcherGroup: self, *, rule: Optional[Union[Rule, T_RuleChecker]] = None, - handlers: Optional[List[T_Handler]] = None, + handlers: Optional[List[Union[T_Handler, Handler]]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -299,7 +300,7 @@ class MatcherGroup: *, rule: Optional[Union[Rule, T_RuleChecker]] = None, permission: Optional[Permission] = None, - handlers: Optional[List[T_Handler]] = None, + handlers: Optional[List[Union[T_Handler, Handler]]] = None, temp: bool = False, priority: int = 1, block: bool = True, @@ -311,7 +312,7 @@ class MatcherGroup: self, *, rule: Optional[Union[Rule, T_RuleChecker]] = None, - handlers: Optional[List[T_Handler]] = None, + handlers: Optional[List[Union[T_Handler, Handler]]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -323,7 +324,7 @@ class MatcherGroup: self, *, rule: Optional[Union[Rule, T_RuleChecker]] = None, - handlers: Optional[List[T_Handler]] = None, + handlers: Optional[List[Union[T_Handler, Handler]]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -337,7 +338,7 @@ class MatcherGroup: *, rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ..., permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -351,7 +352,7 @@ class MatcherGroup: *, rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ..., permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -365,7 +366,7 @@ class MatcherGroup: *, rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ..., permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -380,7 +381,7 @@ class MatcherGroup: *, rule: Optional[Union[Rule, T_RuleChecker]] = ..., permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -396,7 +397,7 @@ class MatcherGroup: *, rule: Optional[Union[Rule, T_RuleChecker]] = ..., permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., @@ -409,9 +410,9 @@ class MatcherGroup: pattern: str, flags: Union[int, re.RegexFlag] = 0, *, - rule: Optional[Rule] = ..., + rule: Optional[Union[Rule, T_RuleChecker]] = ..., permission: Optional[Permission] = ..., - handlers: Optional[List[T_Handler]] = ..., + handlers: Optional[List[Union[T_Handler, Handler]]] = ..., temp: bool = ..., priority: int = ..., block: bool = ..., diff --git a/nonebot/plugin/export.py b/nonebot/plugin/export.py new file mode 100644 index 00000000..20ad4d72 --- /dev/null +++ b/nonebot/plugin/export.py @@ -0,0 +1,60 @@ +from contextvars import ContextVar + +_export: ContextVar["Export"] = ContextVar("_export") + + +class Export(dict): + """ + :说明: + + 插件导出内容以使得其他插件可以获得。 + + :示例: + + .. code-block:: python + + nonebot.export().default = "bar" + + @nonebot.export() + def some_function(): + pass + + # this doesn't work before python 3.9 + # use + # export = nonebot.export(); @export.sub + # instead + # See also PEP-614: https://www.python.org/dev/peps/pep-0614/ + @nonebot.export().sub + def something_else(): + pass + """ + + def __call__(self, func, **kwargs): + self[func.__name__] = func + self.update(kwargs) + return func + + def __setitem__(self, key, value): + super().__setitem__(key, + Export(value) if isinstance(value, dict) else value) + + def __setattr__(self, name, value): + self[name] = Export(value) if isinstance(value, dict) else value + + def __getattr__(self, name): + if name not in self: + self[name] = Export() + return self[name] + + +def export() -> Export: + """ + :说明: + + 获取插件的导出内容对象 + + :返回: + + - ``Export`` + """ + return _export.get() diff --git a/nonebot/plugin/manager.py b/nonebot/plugin/manager.py index 5965d02b..fdbfe43b 100644 --- a/nonebot/plugin/manager.py +++ b/nonebot/plugin/manager.py @@ -5,9 +5,15 @@ import importlib from hashlib import md5 from types import ModuleType from collections import Counter +from contextvars import ContextVar from importlib.abc import MetaPathFinder -from importlib.machinery import PathFinder from typing import Set, List, Optional, Iterable +from importlib.machinery import PathFinder, SourceFileLoader + +from .export import _export, Export + +_current_plugin: ContextVar[Optional[str]] = ContextVar("_current_plugin", + default=None) _internal_space = ModuleType(__name__ + "._internal") _internal_space.__path__ = [] # type: ignore @@ -138,10 +144,12 @@ class PluginManager: def load_plugin(self, name) -> ModuleType: if name in self.plugins: - return importlib.import_module(name) + with self: + return importlib.import_module(name) if "." in name: raise ValueError("Plugin name cannot contain '.'") + with self: return importlib.import_module(f"{self.namespace}.{name}") @@ -149,14 +157,15 @@ class PluginManager: return [self.load_plugin(name) for name in self.list_plugins()] def _rewrite_module_name(self, module_name) -> Optional[str]: - if module_name == self.namespace: - return self.internal_module.__name__ - elif module_name.startswith(self.namespace + "."): + prefix = f"{self.internal_module.__name__}." + if module_name.startswith(self.namespace + "."): path = module_name.split(".") length = self.namespace.count(".") + 1 - return f"{self.internal_module.__name__}.{'.'.join(path[length:])}" + return f"{prefix}{'.'.join(path[length:])}" + elif module_name in self.plugins or module_name.startswith(prefix): + return module_name elif module_name in self.search_plugins(): - return f"{self.internal_module.__name__}.{module_name}" + return f"{prefix}{module_name}" return None @@ -169,13 +178,52 @@ class PluginFinder(MetaPathFinder): manager = _manager_stack[index] newname = manager._rewrite_module_name(fullname) if newname: - spec = PathFinder.find_spec(newname, - list(manager.search_path), - target) + spec = PathFinder.find_spec( + newname, [*manager.search_path, *(path or [])], target) if spec: + spec.loader = PluginLoader(manager, newname, + spec.origin) return spec index -= 1 return None +class PluginLoader(SourceFileLoader): + + def __init__(self, manager: PluginManager, fullname: str, path) -> None: + self.manager = manager + self.loaded = False + self._plugin_token = None + self._export_token = None + super().__init__(fullname, path) + + def create_module(self, spec) -> Optional[ModuleType]: + if self.name in sys.modules: + self.loaded = True + return sys.modules[self.name] + prefix = self.manager.internal_module.__name__ + plugin_name = self.name[len(prefix):] if self.name.startswith( + prefix) else self.name + self._plugin_token = _current_plugin.set(plugin_name.lstrip(".")) + self._export_token = _export.set(Export()) + # return None to use default module creation + return super().create_module(spec) + + def exec_module(self, module: ModuleType) -> None: + if self.loaded: + return + # really need? + # setattr(module, "__manager__", self.manager) + if self._export_token: + setattr(module, "__export__", _export.get()) + + super().exec_module(module) + + if self._plugin_token: + _current_plugin.reset(self._plugin_token) + if self._export_token: + _export.reset(self._export_token) + return + + sys.meta_path.insert(0, PluginFinder()) diff --git a/nonebot/plugins/single_session.py b/nonebot/plugins/single_session.py index 30fdc496..42df70d7 100644 --- a/nonebot/plugins/single_session.py +++ b/nonebot/plugins/single_session.py @@ -17,7 +17,7 @@ async def _(matcher: Matcher, bot: Bot, event: Event, state: T_State): current_event_id = id(event) event_id = _running_matcher.get(session_id, None) if event_id and event_id != current_event_id: - raise IgnoredException("Annother matcher running") + raise IgnoredException("Another matcher running") _running_matcher[session_id] = current_event_id diff --git a/nonebot/rule.py b/nonebot/rule.py index 75022335..72af1601 100644 --- a/nonebot/rule.py +++ b/nonebot/rule.py @@ -287,10 +287,15 @@ class ArgumentParser(ArgParser): """ def _print_message(self, message, file=None): - pass + old_message: str = getattr(self, "message", "") + if old_message: + old_message += "\n" + old_message += message + setattr(self, "message", old_message) def exit(self, status=0, message=None): - raise ParserExit(status=status, message=message) + raise ParserExit(status=status, + message=message or getattr(self, "message", None)) def parse_args(self, args: Optional[Sequence[str]] = None, diff --git a/nonebot/typing.py b/nonebot/typing.py index dd2f24c5..a9dccc35 100644 --- a/nonebot/typing.py +++ b/nonebot/typing.py @@ -71,6 +71,7 @@ T_WebSocketDisconnectionHook = Callable[["Bot"], Awaitable[None]] WebSocket 连接断开时执行的函数 """ +T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[None]] T_EventPreProcessor = Callable[["Bot", "Event", T_State], Awaitable[None]] """ diff --git a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/bot.py b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/bot.py index 3d440127..bea0bcc3 100644 --- a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/bot.py +++ b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/bot.py @@ -52,8 +52,12 @@ async def _check_reply(bot: "Bot", event: "Event"): except ValueError: return msg_seg = event.message[index] - event.reply = Reply.parse_obj(await - bot.get_msg(message_id=msg_seg.data["id"])) + try: + event.reply = Reply.parse_obj(await + bot.get_msg(message_id=msg_seg.data["id"] + )) + except Exception as e: + log("WARNING", f"Error when getting message reply info: {repr(e)}", e) # ensure string comparation if str(event.reply.sender.user_id) == str(event.self_id): event.to_me = True @@ -244,7 +248,7 @@ class Bot(BaseBot): @classmethod @overrides(BaseBot) async def check_permission(cls, driver: "Driver", connection_type: str, - headers: dict, body: Optional[dict]) -> str: + headers: dict, body: Optional[bytes]) -> str: """ :说明: @@ -271,14 +275,13 @@ class Bot(BaseBot): if not x_signature: log("WARNING", "Missing Signature Header") raise RequestDenied(401, "Missing Signature") - sig = hmac.new(secret.encode("utf-8"), - json.dumps(body).encode(), "sha1").hexdigest() + sig = hmac.new(secret.encode("utf-8"), body, "sha1").hexdigest() if x_signature != "sha1=" + sig: log("WARNING", "Signature Header is invalid") raise RequestDenied(403, "Signature is invalid") access_token = cqhttp_config.access_token - if access_token and access_token != token: + if access_token and access_token != token and connection_type == "websocket": log( "WARNING", "Authorization Header is invalid" if token else "Missing Authorization Header") @@ -329,32 +332,7 @@ class Bot(BaseBot): ) @overrides(BaseBot) - async def call_api(self, api: str, **data) -> Any: - """ - :说明: - - 调用 CQHTTP 协议 API - - :参数: - - * ``api: str``: API 名称 - * ``**data: Any``: API 参数 - - :返回: - - - ``Any``: API 调用返回数据 - - :异常: - - - ``NetworkError``: 网络错误 - - ``ActionFailed``: API 调用失败 - """ - if "self_id" in data: - self_id = data.pop("self_id") - if self_id: - bot = self.driver.bots[str(self_id)] - return await bot.call_api(api, **data) - + async def _call_api(self, api: str, **data) -> Any: log("DEBUG", f"Calling API {api}") if self.connection_type == "websocket": seq = ResultStore.get_seq() @@ -397,6 +375,29 @@ class Bot(BaseBot): except httpx.HTTPError: raise NetworkError("HTTP request failed") + @overrides(BaseBot) + async def call_api(self, api: str, **data) -> Any: + """ + :说明: + + 调用 CQHTTP 协议 API + + :参数: + + * ``api: str``: API 名称 + * ``**data: Any``: API 参数 + + :返回: + + - ``Any``: API 调用返回数据 + + :异常: + + - ``NetworkError``: 网络错误 + - ``ActionFailed``: API 调用失败 + """ + return await super().call_api(api, **data) + @overrides(BaseBot) async def send(self, event: Event, diff --git a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/bot.pyi b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/bot.pyi index 7ba09f8a..ad8d459c 100644 --- a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/bot.pyi +++ b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/bot.pyi @@ -68,7 +68,8 @@ class Bot(BaseBot): async def handle_message(self, message: dict): ... - async def call_api(self, api: str, **data) -> Any: + async def call_api(self, api: str, *, self_id: Optional[str], + **data) -> Any: ... async def send(self, event: Event, message: Union[str, Message, diff --git a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/config.py b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/config.py index 8879705a..1a17f853 100644 --- a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/config.py +++ b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/config.py @@ -19,3 +19,4 @@ class Config(BaseModel): class Config: extra = "ignore" + allow_population_by_field_name = True diff --git a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/event.py b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/event.py index 53497870..e0551279 100644 --- a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/event.py +++ b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/event.py @@ -199,6 +199,10 @@ class GroupMessageEvent(MessageEvent): if x.is_text() else f"{escape_tag(str(x))}", self.message)) + '"') + @overrides(MessageEvent) + def get_session_id(self) -> str: + return f"group_{self.group_id}_{self.user_id}" + # Notice Events class NoticeEvent(Event): diff --git a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/message.py b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/message.py index 07c463c5..acdef22e 100644 --- a/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/message.py +++ b/packages/nonebot-adapter-cqhttp/nonebot/adapters/cqhttp/message.py @@ -38,7 +38,8 @@ class MessageSegment(BaseMessageSegment): @overrides(BaseMessageSegment) def __radd__(self, other) -> "Message": - return Message(other) + self + return (MessageSegment.text(other) + if isinstance(other, str) else Message(other)) + self @overrides(BaseMessageSegment) def is_text(self) -> bool: @@ -211,6 +212,11 @@ class Message(BaseMessage): CQHTTP 协议 Message 适配。 """ + def __radd__(self, other: Union[str, MessageSegment, + "Message"]) -> "Message": + result = MessageSegment.text(other) if isinstance(other, str) else other + return super(Message, self).__radd__(result) + @staticmethod @overrides(BaseMessage) def _construct( diff --git a/packages/nonebot-adapter-cqhttp/poetry.lock b/packages/nonebot-adapter-cqhttp/poetry.lock index ca160ee8..142450bc 100644 --- a/packages/nonebot-adapter-cqhttp/poetry.lock +++ b/packages/nonebot-adapter-cqhttp/poetry.lock @@ -111,7 +111,7 @@ reference = "aliyun" [[package]] name = "httpx" -version = "0.17.0" +version = "0.17.1" description = "The next generation HTTP client." category = "main" optional = false @@ -119,7 +119,7 @@ python-versions = ">=3.6" [package.dependencies] certifi = "*" -httpcore = ">=0.12.0,<0.13.0" +httpcore = ">=0.12.1,<0.13" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" @@ -167,7 +167,7 @@ reference = "aliyun" [[package]] name = "nonebot2" -version = "2.0.0-alpha.11" +version = "2.0.0-alpha.12" description = "An asynchronous python bot framework." category = "main" optional = false @@ -226,8 +226,8 @@ reference = "aliyun" [[package]] name = "python-dotenv" -version = "0.15.0" -description = "Add .env support to your django/flask apps in development and deployments" +version = "0.16.0" +description = "Read key-value pairs from a .env file and set them as environment variables" category = "main" optional = false python-versions = "*" @@ -418,7 +418,7 @@ reference = "aliyun" [metadata] lock-version = "1.1" python-versions = "^3.7.3" -content-hash = "8110a56337b3ca1557700161e09ff2d74720b5897f0fac09c4ac5038495194d9" +content-hash = "b8ec196a78675b4098ab7509cbdbd311ffcbcf1ce8b625c589f1e95596801c71" [metadata.files] certifi = [ @@ -460,8 +460,8 @@ httptools = [ {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, ] httpx = [ - {file = "httpx-0.17.0-py3-none-any.whl", hash = "sha256:fe19522f7b0861a1f6ac83306360bb5b7fb1ed64633a1a04a33f04102a1bea60"}, - {file = "httpx-0.17.0.tar.gz", hash = "sha256:4f7ab2fef7f929c5531abd4f413b41ce2c820e3202f2eeee498f2d92b6849f8d"}, + {file = "httpx-0.17.1-py3-none-any.whl", hash = "sha256:d379653bd457e8257eb0df99cb94557e4aac441b7ba948e333be969298cac272"}, + {file = "httpx-0.17.1.tar.gz", hash = "sha256:cc2a55188e4b25272d2bcd46379d300f632045de4377682aa98a8a6069d55967"}, ] idna = [ {file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"}, @@ -500,8 +500,8 @@ pygtrie = [ {file = "pygtrie-2.4.2.tar.gz", hash = "sha256:43205559d28863358dbbf25045029f58e2ab357317a59b11f11ade278ac64692"}, ] python-dotenv = [ - {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, - {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, + {file = "python-dotenv-0.16.0.tar.gz", hash = "sha256:9fa413c37d4652d3fa02fea0ff465c384f5db75eab259c4fc5d0c5b8bf20edd4"}, + {file = "python_dotenv-0.16.0-py2.py3-none-any.whl", hash = "sha256:31d752f5b748f4e292448c9a0cac6a08ed5e6f4cefab85044462dcad56905cec"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, diff --git a/packages/nonebot-adapter-cqhttp/pyproject.toml b/packages/nonebot-adapter-cqhttp/pyproject.toml index 5054bf7b..46697cc7 100644 --- a/packages/nonebot-adapter-cqhttp/pyproject.toml +++ b/packages/nonebot-adapter-cqhttp/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nonebot-adapter-cqhttp" -version = "2.0.0a11.post2" +version = "2.0.0-alpha.12" description = "OneBot(CQHTTP) adapter for nonebot2" authors = ["yanyongyu "] license = "MIT" @@ -25,7 +25,7 @@ exclude = ["nonebot/__init__.py", "nonebot/adapters/__init__.py"] [tool.poetry.dependencies] python = "^3.7.3" httpx = "^0.17.0" -nonebot2 = "^2.0.0-alpha.11" +nonebot2 = "^2.0.0-alpha.12" [tool.poetry.dev-dependencies] nonebot2 = { path = "../../", develop = true } diff --git a/packages/nonebot-adapter-ding/nonebot/adapters/ding/bot.py b/packages/nonebot-adapter-ding/nonebot/adapters/ding/bot.py index cfe73fde..8f2880a1 100644 --- a/packages/nonebot-adapter-ding/nonebot/adapters/ding/bot.py +++ b/packages/nonebot-adapter-ding/nonebot/adapters/ding/bot.py @@ -1,6 +1,8 @@ -import hmac -import base64 +import json +import urllib.parse + from datetime import datetime +import time from typing import Any, Union, Optional, TYPE_CHECKING import httpx @@ -10,7 +12,7 @@ from nonebot.message import handle_event from nonebot.adapters import Bot as BaseBot from nonebot.exception import RequestDenied -from .utils import log +from .utils import calc_hmac_base64, log from .config import Config as DingConfig from .message import Message, MessageSegment from .exception import NetworkError, ApiNotAvailable, ActionFailed, SessionExpired @@ -20,7 +22,7 @@ if TYPE_CHECKING: from nonebot.config import Config from nonebot.drivers import Driver -SEND_BY_SESSION_WEBHOOK = "send_by_sessionWebhook" +SEND = "send" class Bot(BaseBot): @@ -48,7 +50,7 @@ class Bot(BaseBot): @classmethod @overrides(BaseBot) async def check_permission(cls, driver: "Driver", connection_type: str, - headers: dict, body: Optional[dict]) -> str: + headers: dict, body: Optional[bytes]) -> str: """ :说明: @@ -72,15 +74,13 @@ class Bot(BaseBot): if not sign: log("WARNING", "Missing Signature Header") raise RequestDenied(400, "Missing `sign` Header") - string_to_sign = f"{timestamp}\n{secret}" - sig = hmac.new(secret.encode("utf-8"), - string_to_sign.encode("utf-8"), "sha256").digest() - if sign != base64.b64encode(sig).decode("utf-8"): + sign_base64 = calc_hmac_base64(str(timestamp), secret) + if sign != sign_base64.decode('utf-8'): log("WARNING", "Signature Header is invalid") raise RequestDenied(403, "Signature is invalid") else: log("WARNING", "Ding signature check ignored!") - return body["chatbotUserId"] + return json.loads(body.decode())["chatbotUserId"] @overrides(BaseBot) async def handle_message(self, message: dict): @@ -108,6 +108,66 @@ class Bot(BaseBot): ) return + @overrides(BaseBot) + async def _call_api(self, + api: str, + event: Optional[MessageEvent] = None, + **data) -> Any: + if self.connection_type != "http": + log("ERROR", "Only support http connection.") + return + + log("DEBUG", f"Calling API {api}") + params = {} + # 传入参数有 webhook,则使用传入的 webhook + webhook = data.get("webhook") + + if webhook: + secret = data.get("secret") + if secret: + # 有这个参数的时候再计算加签的值 + timestamp = str(round(time.time() * 1000)) + params["timestamp"] = timestamp + hmac_code_base64 = calc_hmac_base64(timestamp, secret) + sign = urllib.parse.quote_plus(hmac_code_base64) + params["sign"] = sign + else: + # webhook 不存在则使用 event 中的 sessionWebhook + if event: + # 确保 sessionWebhook 没有过期 + if int(datetime.now().timestamp()) > int( + event.sessionWebhookExpiredTime / 1000): + raise SessionExpired + + webhook = event.sessionWebhook + else: + raise ApiNotAvailable + + headers = {} + message: Message = data.get("message", None) + if not message: + raise ValueError("Message not found") + try: + async with httpx.AsyncClient(headers=headers) as client: + response = await client.post(webhook, + params=params, + json=message._produce(), + timeout=self.config.api_timeout) + + if 200 <= response.status_code < 300: + result = response.json() + if isinstance(result, dict): + if result.get("errcode") != 0: + raise ActionFailed(errcode=result.get("errcode"), + errmsg=result.get("errmsg")) + return result + raise NetworkError(f"HTTP request received unexpected " + f"status code: {response.status_code}") + except httpx.InvalidURL: + raise NetworkError("API root url invalid") + except httpx.HTTPError: + raise NetworkError("HTTP request failed") + @overrides(BaseBot) async def call_api(self, api: str, @@ -121,6 +181,7 @@ class Bot(BaseBot): :参数: * ``api: str``: API 名称 + * ``event: Optional[MessageEvent]``: Event 对象 * ``**data: Any``: API 参数 :返回: @@ -132,59 +193,15 @@ class Bot(BaseBot): - ``NetworkError``: 网络错误 - ``ActionFailed``: API 调用失败 """ - if self.connection_type != "http": - log("ERROR", "Only support http connection.") - return - if "self_id" in data: - self_id = data.pop("self_id") - if self_id: - bot = self.driver.bots[str(self_id)] - return await bot.call_api(api, **data) - - log("DEBUG", f"Calling API {api}") - - if api == SEND_BY_SESSION_WEBHOOK: - if event: - # 确保 sessionWebhook 没有过期 - if int(datetime.now().timestamp()) > int( - event.sessionWebhookExpiredTime / 1000): - raise SessionExpired - - target = event.sessionWebhook - else: - raise ApiNotAvailable - - headers = {} - message: Message = data.get("message", None) - if not message: - raise ValueError("Message not found") - try: - async with httpx.AsyncClient(headers=headers) as client: - response = await client.post( - target, - params={"access_token": self.ding_config.access_token}, - json=message._produce(), - timeout=self.config.api_timeout) - - if 200 <= response.status_code < 300: - result = response.json() - if isinstance(result, dict): - if result.get("errcode") != 0: - raise ActionFailed(errcode=result.get("errcode"), - errmsg=result.get("errmsg")) - return result - raise NetworkError(f"HTTP request received unexpected " - f"status code: {response.status_code}") - except httpx.InvalidURL: - raise NetworkError("API root url invalid") - except httpx.HTTPError: - raise NetworkError("HTTP request failed") + return await super().call_api(api, event=event, **data) @overrides(BaseBot) async def send(self, event: MessageEvent, message: Union[str, "Message", "MessageSegment"], at_sender: bool = False, + webhook: Optional[str] = None, + secret: Optional[str] = None, **kwargs) -> Any: """ :说明: @@ -196,6 +213,8 @@ class Bot(BaseBot): * ``event: Event``: Event 对象 * ``message: Union[str, Message, MessageSegment]``: 要发送的消息 * ``at_sender: bool``: 是否 @ 事件主体 + * ``webhook: Optional[str]``: 该条消息将调用的 webhook 地址。不传则将使用 sessionWebhook,若其也不存在,该条消息不发送,使用自定义 webhook 时注意你设置的安全方式,如加关键词,IP地址,加签等等。 + * ``secret: Optional[str]``: 如果你使用自定义的 webhook 地址,推荐使用加签方式对消息进行验证,将 `机器人安全设置页面,加签一栏下面显示的SEC开头的字符串` 传入这个参数即可。 * ``**kwargs``: 覆盖默认参数 :返回: @@ -213,6 +232,9 @@ class Bot(BaseBot): at_sender = at_sender and bool(event.senderId) params = {} params["event"] = event + if webhook: + params["webhook"] = webhook + params["secret"] = secret params.update(kwargs) if at_sender and event.conversationType != ConversationType.private: @@ -222,4 +244,4 @@ class Bot(BaseBot): else: params["message"] = msg - return await self.call_api(SEND_BY_SESSION_WEBHOOK, **params) + return await self.call_api(SEND, **params) diff --git a/packages/nonebot-adapter-ding/nonebot/adapters/ding/config.py b/packages/nonebot-adapter-ding/nonebot/adapters/ding/config.py index 08e84ee2..405fb2c5 100644 --- a/packages/nonebot-adapter-ding/nonebot/adapters/ding/config.py +++ b/packages/nonebot-adapter-ding/nonebot/adapters/ding/config.py @@ -17,3 +17,4 @@ class Config(BaseModel): class Config: extra = "ignore" + allow_population_by_field_name = True diff --git a/packages/nonebot-adapter-ding/nonebot/adapters/ding/event.py b/packages/nonebot-adapter-ding/nonebot/adapters/ding/event.py index 7dfdc2e1..e01588a6 100644 --- a/packages/nonebot-adapter-ding/nonebot/adapters/ding/event.py +++ b/packages/nonebot-adapter-ding/nonebot/adapters/ding/event.py @@ -143,3 +143,7 @@ class GroupMessageEvent(MessageEvent): @overrides(MessageEvent) def is_tome(self) -> bool: return self.isInAtList + + @overrides(MessageEvent) + def get_session_id(self) -> str: + return f"group_{self.conversationId}_{self.senderId}" diff --git a/packages/nonebot-adapter-ding/nonebot/adapters/ding/utils.py b/packages/nonebot-adapter-ding/nonebot/adapters/ding/utils.py index eb4145bc..5529d42b 100644 --- a/packages/nonebot-adapter-ding/nonebot/adapters/ding/utils.py +++ b/packages/nonebot-adapter-ding/nonebot/adapters/ding/utils.py @@ -1,3 +1,17 @@ +import hmac +import base64 +import hashlib + from nonebot.utils import logger_wrapper log = logger_wrapper("DING") + + +def calc_hmac_base64(timestamp: str, secret: str): + secret_enc = secret.encode('utf-8') + string_to_sign = '{}\n{}'.format(timestamp, secret) + string_to_sign_enc = string_to_sign.encode('utf-8') + hmac_code = hmac.new(secret_enc, + string_to_sign_enc, + digestmod=hashlib.sha256).digest() + return base64.b64encode(hmac_code) diff --git a/packages/nonebot-adapter-ding/poetry.lock b/packages/nonebot-adapter-ding/poetry.lock index ca160ee8..142450bc 100644 --- a/packages/nonebot-adapter-ding/poetry.lock +++ b/packages/nonebot-adapter-ding/poetry.lock @@ -111,7 +111,7 @@ reference = "aliyun" [[package]] name = "httpx" -version = "0.17.0" +version = "0.17.1" description = "The next generation HTTP client." category = "main" optional = false @@ -119,7 +119,7 @@ python-versions = ">=3.6" [package.dependencies] certifi = "*" -httpcore = ">=0.12.0,<0.13.0" +httpcore = ">=0.12.1,<0.13" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" @@ -167,7 +167,7 @@ reference = "aliyun" [[package]] name = "nonebot2" -version = "2.0.0-alpha.11" +version = "2.0.0-alpha.12" description = "An asynchronous python bot framework." category = "main" optional = false @@ -226,8 +226,8 @@ reference = "aliyun" [[package]] name = "python-dotenv" -version = "0.15.0" -description = "Add .env support to your django/flask apps in development and deployments" +version = "0.16.0" +description = "Read key-value pairs from a .env file and set them as environment variables" category = "main" optional = false python-versions = "*" @@ -418,7 +418,7 @@ reference = "aliyun" [metadata] lock-version = "1.1" python-versions = "^3.7.3" -content-hash = "8110a56337b3ca1557700161e09ff2d74720b5897f0fac09c4ac5038495194d9" +content-hash = "b8ec196a78675b4098ab7509cbdbd311ffcbcf1ce8b625c589f1e95596801c71" [metadata.files] certifi = [ @@ -460,8 +460,8 @@ httptools = [ {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, ] httpx = [ - {file = "httpx-0.17.0-py3-none-any.whl", hash = "sha256:fe19522f7b0861a1f6ac83306360bb5b7fb1ed64633a1a04a33f04102a1bea60"}, - {file = "httpx-0.17.0.tar.gz", hash = "sha256:4f7ab2fef7f929c5531abd4f413b41ce2c820e3202f2eeee498f2d92b6849f8d"}, + {file = "httpx-0.17.1-py3-none-any.whl", hash = "sha256:d379653bd457e8257eb0df99cb94557e4aac441b7ba948e333be969298cac272"}, + {file = "httpx-0.17.1.tar.gz", hash = "sha256:cc2a55188e4b25272d2bcd46379d300f632045de4377682aa98a8a6069d55967"}, ] idna = [ {file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"}, @@ -500,8 +500,8 @@ pygtrie = [ {file = "pygtrie-2.4.2.tar.gz", hash = "sha256:43205559d28863358dbbf25045029f58e2ab357317a59b11f11ade278ac64692"}, ] python-dotenv = [ - {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, - {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, + {file = "python-dotenv-0.16.0.tar.gz", hash = "sha256:9fa413c37d4652d3fa02fea0ff465c384f5db75eab259c4fc5d0c5b8bf20edd4"}, + {file = "python_dotenv-0.16.0-py2.py3-none-any.whl", hash = "sha256:31d752f5b748f4e292448c9a0cac6a08ed5e6f4cefab85044462dcad56905cec"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, diff --git a/packages/nonebot-adapter-ding/pyproject.toml b/packages/nonebot-adapter-ding/pyproject.toml index b11ffded..3f367f81 100644 --- a/packages/nonebot-adapter-ding/pyproject.toml +++ b/packages/nonebot-adapter-ding/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nonebot-adapter-ding" -version = "2.0.0a11.post2" +version = "2.0.0-alpha.12" description = "Ding adapter for nonebot2" authors = ["Artin ", "yanyongyu "] license = "MIT" @@ -25,7 +25,7 @@ exclude = ["nonebot/__init__.py", "nonebot/adapters/__init__.py"] [tool.poetry.dependencies] python = "^3.7.3" httpx = "^0.17.0" -nonebot2 = "^2.0.0-alpha.11" +nonebot2 = "^2.0.0-alpha.12" [tool.poetry.dev-dependencies] nonebot2 = { path = "../../", develop = true } diff --git a/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot.py b/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot.py index 4f5cb196..ebce2d74 100644 --- a/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot.py +++ b/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot.py @@ -178,7 +178,7 @@ class Bot(BaseBot): @classmethod @overrides(BaseBot) async def check_permission(cls, driver: "Driver", connection_type: str, - headers: dict, body: Optional[dict]) -> str: + headers: dict, body: Optional[bytes]) -> str: if connection_type == 'ws': raise RequestDenied( status_code=501, @@ -218,13 +218,17 @@ class Bot(BaseBot): except Exception as e: Log.error(f'Failed to handle message: {message}', e) + @overrides(BaseBot) + async def _call_api(self, api: str, **data) -> NoReturn: + raise NotImplementedError + @overrides(BaseBot) async def call_api(self, api: str, **data) -> NoReturn: """ \:\:\: danger 由于Mirai的HTTP API特殊性, 该API暂时无法实现 \:\:\: - + \:\:\: tip 你可以使用 ``MiraiBot.api`` 中提供的调用方法来代替 \:\:\: @@ -447,7 +451,7 @@ class Bot(BaseBot): :说明: 使用此方法获取bot接收到的最老消息和最老各类事件 - (不会从MiraiApiHttp消息记录中删除) + (不会从MiraiApiHttp消息记录中删除) :参数: @@ -462,7 +466,7 @@ class Bot(BaseBot): 使用此方法获取bot接收到的最新消息和最新各类事件 (不会从MiraiApiHttp消息记录中删除) - + :参数: * ``count: int``: 获取消息和事件的数量 @@ -599,7 +603,7 @@ class Bot(BaseBot): """ :说明: - 使用此方法使Bot退出群聊 + 使用此方法使Bot退出群聊 :参数: diff --git a/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot_ws.py b/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot_ws.py index 9dabe356..7f990183 100644 --- a/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot_ws.py +++ b/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot_ws.py @@ -116,7 +116,8 @@ class WebsocketBot(Bot): @classmethod @overrides(Bot) async def check_permission(cls, driver: "Driver", connection_type: str, - headers: dict, body: Optional[dict]) -> NoReturn: + headers: dict, + body: Optional[bytes]) -> NoReturn: raise RequestDenied( status_code=501, reason=f'Connection {connection_type} not implented') @@ -127,7 +128,7 @@ class WebsocketBot(Bot): """ :说明: - 注册该Adapter + 注册该Adapter :参数: diff --git a/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/config.py b/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/config.py index e1983f45..6783259f 100644 --- a/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/config.py +++ b/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/config.py @@ -20,3 +20,4 @@ class Config(BaseModel): class Config: extra = Extra.ignore + allow_population_by_field_name = True diff --git a/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/utils.py b/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/utils.py index 74ad9f6e..14879170 100644 --- a/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/utils.py +++ b/packages/nonebot-adapter-mirai/nonebot/adapters/mirai/utils.py @@ -11,7 +11,7 @@ from nonebot.message import handle_event from nonebot.utils import escape_tag, logger_wrapper from .event import Event, GroupMessage, MessageEvent, MessageSource -from .message import MessageType +from .message import MessageType, MessageSegment if TYPE_CHECKING: from .bot import Bot @@ -64,7 +64,7 @@ class ActionFailed(exception.ActionFailed): class InvalidArgument(exception.AdapterException): """ :说明: - + 调用API的参数出错 """ @@ -106,7 +106,7 @@ def argument_validation(function: _AnyCallable) -> _AnyCallable: :说明: 通过函数签名中的类型注解来对传入参数进行运行时校验 - + 会在参数出错时释放 ``InvalidArgument`` 异常 """ function = validate_arguments(config={ @@ -138,6 +138,8 @@ def process_at(bot: "Bot", event: GroupMessage) -> GroupMessage: event.to_me = True else: event.message_chain.insert(0, at) + if not event.message_chain: + event.message_chain.append(MessageSegment.plain('')) return event @@ -175,4 +177,4 @@ async def process_event(bot: "Bot", event: Event) -> None: event = process_nick(bot, event) event = process_at(bot, event) event = process_reply(bot, event) - await handle_event(bot, event) \ No newline at end of file + await handle_event(bot, event) diff --git a/packages/nonebot-adapter-mirai/poetry.lock b/packages/nonebot-adapter-mirai/poetry.lock index 8178a3bf..f691f4da 100644 --- a/packages/nonebot-adapter-mirai/poetry.lock +++ b/packages/nonebot-adapter-mirai/poetry.lock @@ -111,7 +111,7 @@ reference = "aliyun" [[package]] name = "httpx" -version = "0.17.0" +version = "0.17.1" description = "The next generation HTTP client." category = "main" optional = false @@ -119,7 +119,7 @@ python-versions = ">=3.6" [package.dependencies] certifi = "*" -httpcore = ">=0.12.0,<0.13.0" +httpcore = ">=0.12.1,<0.13" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" @@ -167,7 +167,7 @@ reference = "aliyun" [[package]] name = "nonebot2" -version = "2.0.0-alpha.11" +version = "2.0.0-alpha.12" description = "An asynchronous python bot framework." category = "main" optional = false @@ -226,8 +226,8 @@ reference = "aliyun" [[package]] name = "python-dotenv" -version = "0.15.0" -description = "Add .env support to your django/flask apps in development and deployments" +version = "0.16.0" +description = "Read key-value pairs from a .env file and set them as environment variables" category = "main" optional = false python-versions = "*" @@ -418,7 +418,7 @@ reference = "aliyun" [metadata] lock-version = "1.1" python-versions = "^3.7.3" -content-hash = "9eb7c46cddf1245508a34c00f5709d21415d02d7f8c356733cc20ad187f431f9" +content-hash = "5b8b3e27eccd897aa33fea94ba813b7d601c5656d52efed4401b398f6d68a677" [metadata.files] certifi = [ @@ -460,8 +460,8 @@ httptools = [ {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, ] httpx = [ - {file = "httpx-0.17.0-py3-none-any.whl", hash = "sha256:fe19522f7b0861a1f6ac83306360bb5b7fb1ed64633a1a04a33f04102a1bea60"}, - {file = "httpx-0.17.0.tar.gz", hash = "sha256:4f7ab2fef7f929c5531abd4f413b41ce2c820e3202f2eeee498f2d92b6849f8d"}, + {file = "httpx-0.17.1-py3-none-any.whl", hash = "sha256:d379653bd457e8257eb0df99cb94557e4aac441b7ba948e333be969298cac272"}, + {file = "httpx-0.17.1.tar.gz", hash = "sha256:cc2a55188e4b25272d2bcd46379d300f632045de4377682aa98a8a6069d55967"}, ] idna = [ {file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"}, @@ -500,8 +500,8 @@ pygtrie = [ {file = "pygtrie-2.4.2.tar.gz", hash = "sha256:43205559d28863358dbbf25045029f58e2ab357317a59b11f11ade278ac64692"}, ] python-dotenv = [ - {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, - {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, + {file = "python-dotenv-0.16.0.tar.gz", hash = "sha256:9fa413c37d4652d3fa02fea0ff465c384f5db75eab259c4fc5d0c5b8bf20edd4"}, + {file = "python_dotenv-0.16.0-py2.py3-none-any.whl", hash = "sha256:31d752f5b748f4e292448c9a0cac6a08ed5e6f4cefab85044462dcad56905cec"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, diff --git a/packages/nonebot-adapter-mirai/pyproject.toml b/packages/nonebot-adapter-mirai/pyproject.toml index e4adc814..a38e36d1 100644 --- a/packages/nonebot-adapter-mirai/pyproject.toml +++ b/packages/nonebot-adapter-mirai/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nonebot-adapter-mirai" -version = "2.0.0a11.post2" +version = "2.0.0-alpha.12" description = "Mirai Api HTTP adapter for nonebot2" authors = ["Mix ", "yanyongyu "] license = "AGPL-3.0-or-later" @@ -26,7 +26,7 @@ exclude = ["nonebot/__init__.py", "nonebot/adapters/__init__.py"] python = "^3.7.3" httpx = "^0.17.0" websockets = "^8.1" -nonebot2 = "^2.0.0-alpha.11" +nonebot2 = "^2.0.0-alpha.12" [tool.poetry.dev-dependencies] nonebot2 = { path = "../../", develop = true } diff --git a/pages/changelog.md b/pages/changelog.md index 713f722d..4315c502 100644 --- a/pages/changelog.md +++ b/pages/changelog.md @@ -4,6 +4,17 @@ sidebar: auto # 更新日志 +## v2.0.0a12 + +- 分离 `handler` 与 `matcher` +- 修复 `cqhttp` secret 校验出错 +- 修复 `pydantic 1.8` 导致的 `alias` 问题 +- 修改 `cqhttp` `ding` `session id`,不再允许跨群 +- 修改 `shell_command` 存储 message +- 修复 `cqhttp` 检查 reply 失败退出 +- 新增 `call_api` hook 接口 +- 优化 `import hook` + ## v2.0.0a11 - 修改 `nonebot` 项目结构,分离所有 `adapter` diff --git a/poetry.lock b/poetry.lock index 73598b33..6f81c694 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3,7 +3,7 @@ name = "aiofiles" version = "0.6.0" description = "File support for asyncio." category = "main" -optional = true +optional = false python-versions = "*" [package.source] @@ -235,7 +235,7 @@ reference = "aliyun" [[package]] name = "httpx" -version = "0.17.0" +version = "0.17.1" description = "The next generation HTTP client." category = "dev" optional = false @@ -243,7 +243,7 @@ python-versions = ">=3.6" [package.dependencies] certifi = "*" -httpcore = ">=0.12.0,<0.13.0" +httpcore = ">=0.12.1,<0.13" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" @@ -389,7 +389,7 @@ reference = "aliyun" [[package]] name = "nonebot-adapter-cqhttp" -version = "2.0.0-alpha.11" +version = "2.0.0-alpha.12" description = "OneBot(CQHTTP) adapter for nonebot2" category = "dev" optional = false @@ -398,7 +398,7 @@ develop = true [package.dependencies] httpx = "^0.17.0" -nonebot2 = "^2.0.0-alpha.11" +nonebot2 = "^2.0.0-alpha.12" [package.source] type = "directory" @@ -406,7 +406,7 @@ url = "packages/nonebot-adapter-cqhttp" [[package]] name = "nonebot-adapter-ding" -version = "2.0.0-alpha.11" +version = "2.0.0-alpha.12" description = "Ding adapter for nonebot2" category = "dev" optional = false @@ -415,7 +415,7 @@ develop = true [package.dependencies] httpx = "^0.17.0" -nonebot2 = "^2.0.0-alpha.11" +nonebot2 = "^2.0.0-alpha.12" [package.source] type = "directory" @@ -423,7 +423,7 @@ url = "packages/nonebot-adapter-ding" [[package]] name = "nonebot-adapter-mirai" -version = "2.0.0-alpha.11" +version = "2.0.0-alpha.12" description = "Mirai Api HTTP adapter for nonebot2" category = "dev" optional = false @@ -432,13 +432,31 @@ develop = true [package.dependencies] httpx = "^0.17.0" -nonebot2 = "^2.0.0-alpha.11" +nonebot2 = "^2.0.0-alpha.12" websockets = "^8.1" [package.source] type = "directory" url = "packages/nonebot-adapter-mirai" +[[package]] +name = "nonebot-plugin-test" +version = "0.2.0" +description = "Test frontend for nonebot v2+" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +aiofiles = ">=0.6.0,<0.7.0" +nonebot2 = ">=2.0.0-alpha.9,<3.0.0" +python-socketio = ">=4.6.1,<5.0.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + [[package]] name = "packaging" version = "20.9" @@ -491,14 +509,14 @@ reference = "aliyun" [[package]] name = "pydash" -version = "4.9.3" +version = "5.0.0" description = "The kitchen sink of Python utility libraries for doing \"stuff\" in a functional way. Based on the Lo-Dash Javascript library." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [package.extras] -dev = ["coverage", "docformatter", "flake8", "invoke", "mock", "pylint", "pytest", "pytest-cov", "pytest-flake8", "pytest-pylint", "sphinx", "sphinx-rtd-theme", "tox", "twine", "wheel", "black", "flake8-black", "flake8-bugbear", "flake8-isort", "isort"] +dev = ["black", "coverage", "docformatter", "flake8", "flake8-black", "flake8-bugbear", "flake8-isort", "invoke", "isort", "pylint", "pytest", "pytest-cov", "pytest-flake8", "pytest-pylint", "sphinx", "sphinx-rtd-theme", "tox", "twine", "wheel"] [package.source] type = "legacy" @@ -507,7 +525,7 @@ reference = "aliyun" [[package]] name = "pygments" -version = "2.8.0" +version = "2.8.1" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -546,8 +564,8 @@ reference = "aliyun" [[package]] name = "python-dotenv" -version = "0.15.0" -description = "Add .env support to your django/flask apps in development and deployments" +version = "0.16.0" +description = "Read key-value pairs from a .env file and set them as environment variables" category = "main" optional = false python-versions = "*" @@ -560,6 +578,47 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "aliyun" +[[package]] +name = "python-engineio" +version = "3.14.2" +description = "Engine.IO server" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +asyncio_client = ["aiohttp (>=3.4)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + +[[package]] +name = "python-socketio" +version = "4.6.1" +description = "Socket.IO server" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +python-engineio = ">=3.13.0,<4" +six = ">=1.9.0" + +[package.extras] +asyncio_client = ["aiohttp (>=3.4)", "websockets (>=7.0)"] +client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + [[package]] name = "pytz" version = "2021.1" @@ -655,6 +714,19 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "aliyun" +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "aliyun" + [[package]] name = "sniffio" version = "1.2.0" @@ -683,7 +755,7 @@ reference = "aliyun" [[package]] name = "sphinx" -version = "3.5.1" +version = "3.5.3" description = "Python documentation generator" category = "dev" optional = false @@ -926,16 +998,16 @@ reference = "aliyun" [[package]] name = "urllib3" -version = "1.26.3" +version = "1.26.4" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotlipy (>=0.6.0)"] [package.source] type = "legacy" @@ -1083,7 +1155,7 @@ quart = ["Quart"] [metadata] lock-version = "1.1" python-versions = "^3.7.3" -content-hash = "f4e49d25ac5c37b3c7527935b52acf5d0d6d0261a645f4247120f9e28c5ca282" +content-hash = "9907ba758206e19d2c36493252f8b026db796ff4dbc67142824d86da31763919" [metadata.files] aiofiles = [ @@ -1160,8 +1232,8 @@ httptools = [ {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, ] httpx = [ - {file = "httpx-0.17.0-py3-none-any.whl", hash = "sha256:fe19522f7b0861a1f6ac83306360bb5b7fb1ed64633a1a04a33f04102a1bea60"}, - {file = "httpx-0.17.0.tar.gz", hash = "sha256:4f7ab2fef7f929c5531abd4f413b41ce2c820e3202f2eeee498f2d92b6849f8d"}, + {file = "httpx-0.17.1-py3-none-any.whl", hash = "sha256:d379653bd457e8257eb0df99cb94557e4aac441b7ba948e333be969298cac272"}, + {file = "httpx-0.17.1.tar.gz", hash = "sha256:cc2a55188e4b25272d2bcd46379d300f632045de4377682aa98a8a6069d55967"}, ] hypercorn = [ {file = "Hypercorn-0.11.2-py3-none-any.whl", hash = "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821"}, @@ -1248,6 +1320,10 @@ markupsafe = [ nonebot-adapter-cqhttp = [] nonebot-adapter-ding = [] nonebot-adapter-mirai = [] +nonebot-plugin-test = [ + {file = "nonebot-plugin-test-0.2.0.tar.gz", hash = "sha256:c9ee997c5c96160de4af02d10a7c6301b3fc4e942df7e70906df0534606ea23b"}, + {file = "nonebot_plugin_test-0.2.0-py3-none-any.whl", hash = "sha256:75cd18cc282815a03250bb86c7d2a8d6a66a5064ac335bedc9a3e268a1e7dd13"}, +] packaging = [ {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, @@ -1281,12 +1357,12 @@ pydantic = [ {file = "pydantic-1.8.1.tar.gz", hash = "sha256:26cf3cb2e68ec6c0cfcb6293e69fb3450c5fd1ace87f46b64f678b0d29eac4c3"}, ] pydash = [ - {file = "pydash-4.9.3-py2.py3-none-any.whl", hash = "sha256:7851a2d749e70c02585ae4803b01c5e0f47b7ec9df9b84ccb16aac38cad2fdd2"}, - {file = "pydash-4.9.3.tar.gz", hash = "sha256:d709e57b537b1aaf118f188da3ec6242a665090ecd7839b66f857ee3dc2bb006"}, + {file = "pydash-5.0.0-py3-none-any.whl", hash = "sha256:0d87f879a3df4ad9389ab6d63c69eea078517d41541ddd5744cfcff3396e8543"}, + {file = "pydash-5.0.0.tar.gz", hash = "sha256:845262df83b5411742e5f7f7dbfa5ed4d0ddac6d7d0a13c4375c6a3c40d4e8f4"}, ] pygments = [ - {file = "Pygments-2.8.0-py3-none-any.whl", hash = "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88"}, - {file = "Pygments-2.8.0.tar.gz", hash = "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0"}, + {file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"}, + {file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"}, ] pygtrie = [ {file = "pygtrie-2.4.2.tar.gz", hash = "sha256:43205559d28863358dbbf25045029f58e2ab357317a59b11f11ade278ac64692"}, @@ -1296,8 +1372,16 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] python-dotenv = [ - {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, - {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, + {file = "python-dotenv-0.16.0.tar.gz", hash = "sha256:9fa413c37d4652d3fa02fea0ff465c384f5db75eab259c4fc5d0c5b8bf20edd4"}, + {file = "python_dotenv-0.16.0-py2.py3-none-any.whl", hash = "sha256:31d752f5b748f4e292448c9a0cac6a08ed5e6f4cefab85044462dcad56905cec"}, +] +python-engineio = [ + {file = "python-engineio-3.14.2.tar.gz", hash = "sha256:eab4553f2804c1ce97054c8b22cf0d5a9ab23128075248b97e1a5b2f29553085"}, + {file = "python_engineio-3.14.2-py2.py3-none-any.whl", hash = "sha256:5a9e6086d192463b04a1428ff1f85b6ba631bbb19d453b144ffc04f530542b84"}, +] +python-socketio = [ + {file = "python-socketio-4.6.1.tar.gz", hash = "sha256:cd1f5aa492c1eb2be77838e837a495f117e17f686029ebc03d62c09e33f4fa10"}, + {file = "python_socketio-4.6.1-py2.py3-none-any.whl", hash = "sha256:5a21da53fdbdc6bb6c8071f40e13d100e0b279ad997681c2492478e06f370523"}, ] pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, @@ -1338,6 +1422,10 @@ rfc3986 = [ {file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"}, {file = "rfc3986-1.4.0.tar.gz", hash = "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d"}, ] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] sniffio = [ {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, @@ -1347,8 +1435,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, ] sphinx = [ - {file = "Sphinx-3.5.1-py3-none-any.whl", hash = "sha256:e90161222e4d80ce5fc811ace7c6787a226b4f5951545f7f42acf97277bfc35c"}, - {file = "Sphinx-3.5.1.tar.gz", hash = "sha256:11d521e787d9372c289472513d807277caafb1684b33eb4f08f7574c405893a9"}, + {file = "Sphinx-3.5.3-py3-none-any.whl", hash = "sha256:3f01732296465648da43dec8fb40dc451ba79eb3e2cc5c6d79005fd98197107d"}, + {file = "Sphinx-3.5.3.tar.gz", hash = "sha256:ce9c228456131bab09a3d7d10ae58474de562a6f79abb3dc811ae401cf8c1abc"}, ] sphinx-markdown-builder = [] sphinxcontrib-applehelp = [ @@ -1399,8 +1487,8 @@ untokenize = [ {file = "untokenize-0.1.1.tar.gz", hash = "md5:50d325dff09208c624cc603fad33bb0d"}, ] urllib3 = [ - {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, - {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, + {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, + {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, ] uvicorn = [ {file = "uvicorn-0.13.4-py3-none-any.whl", hash = "sha256:7587f7b08bd1efd2b9bad809a3d333e972f1d11af8a5e52a9371ee3a5de71524"}, diff --git a/pyproject.toml b/pyproject.toml index 7e64c234..e73b3a6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nonebot2" -version = "2.0.0-alpha.11" +version = "2.0.0-alpha.12" description = "An asynchronous python bot framework." authors = ["yanyongyu "] license = "MIT" @@ -35,6 +35,7 @@ uvicorn = { version = "^0.13.0", extras = ["standard"] } [tool.poetry.dev-dependencies] yapf = "^0.30.0" sphinx = "^3.4.1" +nonebot-plugin-test = "^0.2.0" nonebot-adapter-cqhttp = { path = "./packages/nonebot-adapter-cqhttp", develop = true } nonebot-adapter-ding = { path = "./packages/nonebot-adapter-ding", develop = true } nonebot-adapter-mirai = { path = "./packages/nonebot-adapter-mirai", develop = true } diff --git a/tests/test_plugins/test_api_hook.py b/tests/test_plugins/test_api_hook.py new file mode 100644 index 00000000..1849daf9 --- /dev/null +++ b/tests/test_plugins/test_api_hook.py @@ -0,0 +1,6 @@ +from nonebot.adapters import Bot + + +@Bot.on_calling_api +async def call(bot: Bot, api: str, data: dict): + print(type(bot), api, data) diff --git a/tests/test_plugins/test_delete.py b/tests/test_plugins/test_delete.py index 2c3265d8..4530771d 100644 --- a/tests/test_plugins/test_delete.py +++ b/tests/test_plugins/test_delete.py @@ -24,7 +24,7 @@ async def test_b(bot: Bot, event: Event, state: T_State): print("======== B Running Completed ========") -c = on_message(priority=0, permission=USER(1111111111)) +c = on_message(priority=0, permission=USER("1111111111")) @c.handle() diff --git a/tests/test_plugins/test_ding.py b/tests/test_plugins/test_ding.py index 96c905d5..ad9b8d4c 100644 --- a/tests/test_plugins/test_ding.py +++ b/tests/test_plugins/test_ding.py @@ -3,6 +3,30 @@ from nonebot.rule import to_me from nonebot.plugin import on_command from nonebot.adapters.ding import Bot as DingBot, MessageSegment, MessageEvent +helper = on_command("ding_helper", to_me()) + + +@helper.handle() +async def ding_helper(bot: DingBot, event: MessageEvent): + message = MessageSegment.markdown( + "Hello, This is NoneBot", + """帮助信息如下:\n +[ding_helper](dtmd://dingtalkclient/sendMessage?content=ding_helper) 查看帮助\n +[markdown](dtmd://dingtalkclient/sendMessage?content=markdown) 发送 markdown\n +[actionCardSingleBtn](dtmd://dingtalkclient/sendMessage?content=actionCardSingleBtn)\n +[actionCard](dtmd://dingtalkclient/sendMessage?content=actionCard)\n +[feedCard](dtmd://dingtalkclient/sendMessage?content=feedCard)\n +[atme](dtmd://dingtalkclient/sendMessage?content=atme)\n +[image](dtmd://dingtalkclient/sendMessage?content=image)\n +[t](dtmd://dingtalkclient/sendMessage?content=t)\n +[code](dtmd://dingtalkclient/sendMessage?content=code) 发送代码\n +[test_message](dtmd://dingtalkclient/sendMessage?content=test_message)\n +[hello](dtmd://dingtalkclient/sendMessage?content=hello)\n +[webhook](dtmd://dingtalkclient/sendMessage?content=webhook)""", + ) + await markdown.finish(message) + + markdown = on_command("markdown", to_me()) @@ -184,3 +208,30 @@ async def hello_handler(bot: DingBot, event: MessageEvent): message = MessageSegment.text(f"@{event.senderId},你好") message += MessageSegment.atDingtalkIds(event.senderId) await hello.finish(message) + + +hello = on_command("webhook", to_me()) + + +@hello.handle() +async def webhook_handler(bot: DingBot, event: MessageEvent): + print(event) + message = MessageSegment.raw({ + "msgtype": "text", + "text": { + "content": 'hello from webhook,一定要注意安全方式的鉴权哦,否则可能发送失败的' + }, + }) + message += MessageSegment.atDingtalkIds(event.senderId) + await hello.send( + message, + webhook= + "https://oapi.dingtalk.com/robot/send?access_token=XXXXXXXXXXXXXX", + secret="SECXXXXXXXXXXXXXXXXXXXXXXXXX") + + message = MessageSegment.text("TEST 123123 S") + await hello.send( + message, + webhook= + "https://oapi.dingtalk.com/robot/send?access_token=XXXXXXXXXXXXXX", + )