🔀 Merge pull request #10 from nonebot/dev

This commit is contained in:
Ju4tCode 2020-09-24 00:37:21 +08:00 committed by GitHub
commit f8231ec7e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 4396 additions and 388 deletions

View File

@ -18,7 +18,7 @@
**NoneBot2 尚在开发中** **NoneBot2 尚在开发中**
NoneBot2 是一个可扩展的 Python 异步机器人框架,它会对机器人收到的消息进行解析和处理,并以插件化的形式,分发给消息所对应的命令处理器和自然语言处理器,来完成具体的功能。 NoneBot2 是一个可扩展的 Python 异步机器人框架,它会对机器人收到的消息进行解析和处理,并以插件化的形式,分发给消息所对应的命令处理器和自然语言处理器,来完成具体的功能。
除了起到解析消息的作用NoneBot 还为插件提供了大量实用的预设操作和权限控制机制,尤其对于命令处理器,它更是提供了完善且易用的会话机制和内部调用机制,以分别适应命令的连续交互和插件内部功能复用等需求。 除了起到解析消息的作用NoneBot 还为插件提供了大量实用的预设操作和权限控制机制,尤其对于命令处理器,它更是提供了完善且易用的会话机制和内部调用机制,以分别适应命令的连续交互和插件内部功能复用等需求。
@ -30,10 +30,10 @@ NoneBot2 是一个可扩展的的 Python 异步机器人框架,它会对机器
## 文档 ## 文档
文档目前尚未完成「API」部分由 sphinx 自动生成,你可以在 [这里](https://docs.nonebot.dev/) 查看。 文档目前尚未完成「API」部分由 sphinx 自动生成,你可以在 [这里](https://v2.nonebot.dev/) 查看。
## 贡献 ## 贡献
如果你在使用过程中发现任何问题,可以 [提交 issue](https://github.com/nonebot/nonebot/issues/new) 或自行 fork 修改后提交 pull request。 如果你在使用过程中发现任何问题,可以 [提交 issue](https://github.com/nonebot/nonebot2/issues/new) 或自行 fork 修改后提交 pull request。
如果你要提交 pull request请确保你的代码风格和项目已有的代码保持一致遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/),变量命名清晰,有适当的注释。 如果你要提交 pull request请确保你的代码风格和项目已有的代码保持一致遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/),变量命名清晰,有适当的注释。

15
archive/2.0.0a1/README.md Normal file
View File

@ -0,0 +1,15 @@
---
home: true
heroImage: /logo.png
tagline: An asynchronous QQ bot framework.
actionText: 开始使用
actionLink: /guide/
features:
- title: 简洁
details: 提供极其简洁易懂的 API使你可以毫无压力地开始验证你的绝佳创意只需编写最少量的代码即可实现丰富的功能。
- title: 易于扩展
details: 精心设计的消息处理流程使得你可以很方便地将原型扩充为具有大量实用功能的完整聊天机器人,并持续保证扩展性。
- title: 高性能
details: 采用异步 I/O利用 WebSocket 进行通信,以获得极高的性能;同时,支持使用多账号同时接入,减少业务宕机的可能。
footer: MIT Licensed | Copyright © 2020 NoneBot Team
---

View File

@ -0,0 +1,31 @@
# NoneBot Api Reference
* **模块索引**
* [nonebot](nonebot.html)
* [nonebot.typing](typing.html)
* [nonebot.config](config.html)
* [nonebot.sched](sched.html)
* [nonebot.log](log.html)
* [nonebot.rule](rule.html)
* [nonebot.permission](permission.html)
* [nonebot.utils](utils.html)
* [nonebot.exception](exception.html)

View File

@ -0,0 +1,265 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.config 模块
## 配置
NoneBot 使用 [pydantic](https://pydantic-docs.helpmanual.io/) 以及 [python-dotenv](https://saurabh-kumar.com/python-dotenv/) 来读取配置。
配置项需符合特殊格式或 json 序列化格式。详情见 [pydantic Field Type](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
## _class_ `Env`
基类:`pydantic.env_settings.BaseSettings`
运行环境配置。大小写不敏感。
将会从 `nonebot.init 参数` > `环境变量` > `.env 环境配置文件` 的优先级读取配置。
### `environment`
* 类型: `str`
* 默认值: `"prod"`
* 说明:
当前环境名。 NoneBot 将从 `.env.{environment}` 文件中加载配置。
## _class_ `Config`
基类:`nonebot.config.BaseConfig`
NoneBot 主要配置。大小写不敏感。
除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。
这些配置将会在 json 反序列化后一起带入 `Config` 类中。
### `driver`
* 类型: `str`
* 默认值: `"nonebot.drivers.fastapi"`
* 说明:
NoneBot 运行所使用的 `Driver` 。继承自 `nonebot.driver.BaseDriver`
### `host`
* 类型: `IPvAnyAddress`
* 默认值: `127.0.0.1`
* 说明:
NoneBot 的 HTTP 和 WebSocket 服务端监听的 IP/主机名。
### `port`
* 类型: `int`
* 默认值: `8080`
* 说明:
NoneBot 的 HTTP 和 WebSocket 服务端监听的端口。
### `debug`
* 类型: `bool`
* 默认值: `False`
* 说明:
是否以调试模式运行 NoneBot。
### `api_root`
* 类型: `Dict[str, str]`
* 默认值: `{}`
* 说明:
以机器人 ID 为键,上报地址为值的字典,环境变量或文件中应使用 json 序列化。
* 示例:
```default
API_ROOT={"123456": "http://127.0.0.1:5700"}
```
### `api_timeout`
* 类型: `Optional[float]`
* 默认值: `30.`
* 说明:
API 请求超时时间,单位: 秒。
### `access_token`
* 类型: `Optional[str]`
* 默认值: `None`
* 说明:
API 请求以及上报所需密钥,在请求头中携带。
* 示例:
```http
POST /cqhttp/ HTTP/1.1
Authorization: Bearer kSLuTF2GC2Q4q4ugm3
```
### `secret`
* 类型: `Optional[str]`
* 默认值: `None`
* 说明:
HTTP POST 形式上报所需签名,在请求头中携带。
* 示例:
```http
POST /cqhttp/ HTTP/1.1
X-Signature: sha1=f9ddd4863ace61e64f462d41ca311e3d2c1176e2
```
### `superusers`
* 类型: `Set[int]`
* 默认值: `set()`
* 说明:
机器人超级用户。
* 示例:
```default
SUPER_USERS=[12345789]
```
### `nickname`
* 类型: `Union[str, Set[str]]`
* 默认值: `""`
* 说明:
机器人昵称。
### `command_start`
* 类型: `Set[str]`
* 默认值: `{"/"}`
* 说明:
命令的起始标记,用于判断一条消息是不是命令。
### `command_sep`
* 类型: `Set[str]`
* 默认值: `{"."}`
* 说明:
命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。
### `session_expire_timeout`
* 类型: `timedelta`
* 默认值: `timedelta(minutes=2)`
* 说明:
等待用户回复的超时时间。
* 示例:
```default
SESSION_EXPIRE_TIMEOUT=120 # 单位: 秒
SESSION_EXPIRE_TIMEOUT=[DD ][HH:MM]SS[.ffffff]
SESSION_EXPIRE_TIMEOUT=P[DD]DT[HH]H[MM]M[SS]S # ISO 8601
```
### `apscheduler_config`
* 类型: `dict`
* 默认值: `{"apscheduler.timezone": "Asia/Shanghai"}`
* 说明:
APScheduler 的配置对象,见 [Configuring the Scheduler](https://apscheduler.readthedocs.io/en/latest/userguide.html#configuring-the-scheduler)

View File

@ -0,0 +1,156 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.exception 模块
## 异常
下列文档中的异常是所有 NoneBot 运行时可能会抛出的。
这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。
## _exception_ `IgnoredException`
基类:`Exception`
* **说明**
指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。
* **参数**
* `reason`: 忽略事件的原因
## _exception_ `PausedException`
基类:`Exception`
* **说明**
指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`
可用于用户输入新信息。
* **用法**
可以在 `Handler` 中通过 `Matcher.pause()` 抛出。
## _exception_ `RejectedException`
基类:`Exception`
* **说明**
指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`
可用于用户重新输入。
* **用法**
可以在 `Handler` 中通过 `Matcher.reject()` 抛出。
## _exception_ `FinishedException`
基类:`Exception`
* **说明**
指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。
可用于结束用户会话。
* **用法**
可以在 `Handler` 中通过 `Matcher.finish()` 抛出。
## _exception_ `ExpiredException`
基类:`Exception`
* **说明**
指示 NoneBot 当前 `Matcher` 已失效。
* **用法**
`Matcher` 运行前检查时抛出。
## _exception_ `StopPropagation`
基类:`Exception`
* **说明**
指示 NoneBot 终止事件向下层传播。
* **用法**
`Matcher.block == True` 时抛出。
## _exception_ `ApiNotAvailable`
基类:`Exception`
* **说明**
在 API 连接不可用时抛出。
## _exception_ `NetworkError`
基类:`Exception`
* **说明**
在网络出现问题时抛出,如: API 请求地址不正确, API 请求无返回或返回状态非正常等。
## _exception_ `ActionFailed`
基类:`Exception`
* **说明**
API 请求成功返回数据,但 API 操作失败。
* **参数**
* `retcode`: 错误代码

View File

@ -0,0 +1,55 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.log 模块
## 日志
NoneBot 使用 [loguru](https://github.com/Delgan/loguru) 来记录日志信息。
自定义 logger 请参考 [loguru](https://github.com/Delgan/loguru) 文档。
## `logger`
* **说明**
NoneBot 日志记录器对象。
* **默认信息**
* 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s`
* 等级: `DEBUG` / `INFO` ,根据 config 配置改变
* 输出: 输出至 stdout
* **用法**
```python
from nonebot.log import logger
```
## _class_ `LoguruHandler`
基类:`logging.Handler`
### `emit(record)`
Do whatever it takes to actually log the specified logging record.
This version is intended to be implemented by subclasses and so
raises a NotImplementedError.

View File

@ -0,0 +1,208 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot 模块
## `get_driver()`
* **说明**
获取全局 Driver 对象。可用于在计划任务的回调中获取当前 Driver 对象。
* **返回**
* `Driver`: 全局 Driver 对象
* **异常**
* `ValueError`: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
* **用法**
```python
driver = nonebot.get_driver()
```
## `get_app()`
* **说明**
获取全局 Driver 对应 Server App 对象。
* **返回**
* `Any`: Server App 对象
* **异常**
* `ValueError`: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
* **用法**
```python
app = nonebot.get_app()
```
## `get_asgi()`
* **说明**
获取全局 Driver 对应 Asgi 对象。
* **返回**
* `Any`: Asgi 对象
* **异常**
* `ValueError`: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
* **用法**
```python
asgi = nonebot.get_asgi()
```
## `get_bots()`
* **说明**
获取所有通过 ws 连接 NoneBot 的 Bot 对象。
* **返回**
* `Dict[str, Bot]`: 一个以字符串 ID 为键Bot 对象为值的字典
* **异常**
* `ValueError`: 全局 Driver 对象尚未初始化 (nonebot.init 尚未调用)
* **用法**
```python
bots = nonebot.get_bots()
```
## `init(*, _env_file=None, **kwargs)`
* **说明**
初始化 NoneBot 以及 全局 Driver 对象。
NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。
你也可以传入自定义的 _env_file 来指定 NoneBot 从该文件读取配置。
* **参数**
* `_env_file: Optional[str]`: 配置文件名,默认从 .env.{env_name} 中读取配置
* `**kwargs`: 任意变量,将会存储到 Config 对象里
* **返回**
* `None`
* **用法**
```python
nonebot.init(database=Database(...))
```
## `run(host=None, port=None, *args, **kwargs)`
* **说明**
启动 NoneBot即运行全局 Driver 对象。
* **参数**
* `host: Optional[str]`: 主机名IP若不传入则使用配置文件中指定的值
* `port: Optional[int]`: 端口,若不传入则使用配置文件中指定的值
* `*args`: 传入 Driver.run 的位置参数
* `**kwargs`: 传入 Driver.run 的命名参数
* **返回**
* `None`
* **用法**
```python
nonebot.run(host="127.0.0.1", port=8080)
```

View File

@ -0,0 +1,121 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.permission 模块
## 权限
每个 `Matcher` 拥有一个 `Permission` ,其中是 **异步** `PermissionChecker` 的集合,只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
:::tip 提示
`PermissionChecker` 既可以是 async function 也可以是 sync function
:::
## `MESSAGE`
* **说明**: 匹配任意 `message` 类型事件,仅在需要同时捕获不同类型事件时使用。优先使用 message type 的 Matcher。
## `NOTICE`
* **说明**: 匹配任意 `notice` 类型事件,仅在需要同时捕获不同类型事件时使用。优先使用 notice type 的 Matcher。
## `REQUEST`
* **说明**: 匹配任意 `request` 类型事件,仅在需要同时捕获不同类型事件时使用。优先使用 request type 的 Matcher。
## `METAEVENT`
* **说明**: 匹配任意 `meta_event` 类型事件,仅在需要同时捕获不同类型事件时使用。优先使用 meta_event type 的 Matcher。
## `USER(*user, perm=<nonebot.permission.Permission object>)`
* **说明**
在白名单内且满足 perm
* **参数**
* `*user: int`: 白名单
* `perm: Permission`: 需要同时满足的权限
## `PRIVATE`
* **说明**: 匹配任意私聊消息类型事件
## `PRIVATE_FRIEND`
* **说明**: 匹配任意好友私聊消息类型事件
## `PRIVATE_GROUP`
* **说明**: 匹配任意群临时私聊消息类型事件
## `PRIVATE_OTHER`
* **说明**: 匹配任意其他私聊消息类型事件
## `GROUP`
* **说明**: 匹配任意群聊消息类型事件
## `GROUP_MEMBER`
* **说明**: 匹配任意群员群聊消息类型事件
:::warning 警告
该权限通过 event.sender 进行判断且不包含管理员以及群主!
:::
## `GROUP_ADMIN`
* **说明**: 匹配任意群管理员群聊消息类型事件
## `GROUP_OWNER`
* **说明**: 匹配任意群主群聊消息类型事件
## `SUPERUSER`
* **说明**: 匹配任意超级用户消息类型事件
## `EVERYBODY`
* **说明**: 匹配任意消息类型事件

122
archive/2.0.0a1/api/rule.md Normal file
View File

@ -0,0 +1,122 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.rule 模块
## 规则
每个 `Matcher` 拥有一个 `Rule` ,其中是 **异步** `RuleChecker` 的集合,只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
:::tip 提示
`RuleChecker` 既可以是 async function 也可以是 sync function
:::
## _class_ `Rule`
基类:`object`
* **说明**
`Matcher` 规则类,当事件传递时,在 `Matcher` 运行前进行检查。
* **示例**
```python
Rule(async_function) & sync_function
# 等价于
from nonebot.utils import run_sync
Rule(async_function, run_sync(sync_function))
```
### `__init__(*checkers)`
* **参数**
* `*checkers: Callable[[Bot, Event, dict], Awaitable[bool]]`: **异步** RuleChecker
### `checkers`
* **说明**
存储 `RuleChecker`
* **类型**
* `Set[Callable[[Bot, Event, dict], Awaitable[bool]]]`
### _async_ `__call__(bot, event, state)`
* **说明**
检查是否符合所有规则
* **参数**
* `bot: Bot`: Bot 对象
* `event: Event`: Event 对象
* `state: dict`: 当前 State
* **返回**
* `bool`
## `startswith(msg)`
* **说明**
匹配消息开头
* **参数**
* `msg: str`: 消息开头字符串
## `endswith(msg)`
* **说明**
匹配消息结尾
* **参数**
* `msg: str`: 消息结尾字符串

View File

@ -0,0 +1,41 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.sched 模块
## 计划任务
计划任务使用第三方库 [APScheduler](https://github.com/agronholm/apscheduler) ,使用文档请参考 [APScheduler使用文档](https://apscheduler.readthedocs.io/en/latest/) 。
## `scheduler`
* **类型**
`Optional[apscheduler.schedulers.asyncio.AsyncIOScheduler]`
* **说明**
当可选依赖 `APScheduler` 未安装时,`scheduler` 为 None
使用 `pip install nonebot[scheduler]` 安装可选依赖
* **常用示例**
```python
from nonebot import scheduler
@scheduler.scheduled_job("cron", hour="*/2", id="xxx", args=[1], kwargs={arg2: 2})
async def run_every_2_hour(arg1, arg2):
pass
scheduler.add_job(run_every_day_from_program_start, "interval", days=1, id="xxx")
```

View File

@ -0,0 +1,236 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.typing 模块
## 类型
下面的文档中,「类型」部分使用 Python 的 Type Hint 语法,见 [PEP 484](https://www.python.org/dev/peps/pep-0484/)、[PEP 526](https://www.python.org/dev/peps/pep-0526/) 和 [typing](https://docs.python.org/3/library/typing.html)。
除了 Python 内置的类型,下面还出现了如下 NoneBot 自定类型,实际上它们是 Python 内置类型的别名。
以下类型均可从 nonebot.typing 模块导入。
## `Driver`
* **类型**
`BaseDriver`
* **说明**
所有 Driver 的基类。
## `WebSocket`
* **类型**
`BaseWebSocket`
* **说明**
所有 WebSocket 的基类。
## `Bot`
* **类型**
`BaseBot`
* **说明**
所有 Bot 的基类。
## `Event`
* **类型**
`BaseEvent`
* **说明**
所有 Event 的基类。
## `Message`
* **类型**
`BaseMessage`
* **说明**
所有 Message 的基类。
## `MessageSegment`
* **类型**
`BaseMessageSegment`
* **说明**
所有 MessageSegment 的基类。
## `PreProcessor`
* **类型**
`Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]`
* **说明**
消息预处理函数 PreProcessor 类型
## `Matcher`
* **类型**
`Matcher`
* **说明**
Matcher 即响应事件的处理类。通过 Rule 判断是否响应事件,运行 Handler。
## `Rule`
* **类型**
`Rule`
* **说明**
Rule 即判断是否响应事件的处理类。内部存储 RuleChecker ,返回全为 True 则响应事件。
## `RuleChecker`
* **类型**
`Callable[[Bot, Event, dict], Union[bool, Awaitable[bool]]]`
* **说明**
RuleChecker 即判断是否响应事件的处理函数。
## `Permission`
* **类型**
`Permission`
* **说明**
Permission 即判断是否响应消息的处理类。内部存储 PermissionChecker ,返回只要有一个 True 则响应消息。
## `PermissionChecker`
* **类型**
`Callable[[Bot, Event], Union[bool, Awaitable[bool]]]`
* **说明**
RuleChecker 即判断是否响应消息的处理函数。
## `Handler`
* **类型**
`Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]`
* **说明**
Handler 即事件的处理函数。
## `ArgsParser`
* **类型**
`Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]`
* **说明**
ArgsParser 即消息参数解析函数,在 Matcher.got 获取参数时被运行。

View File

@ -0,0 +1,39 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.utils 模块
## `run_sync(func)`
* **说明**
一个用于包装 sync function 为 async function 的装饰器
* **参数**
* `func: Callable[..., Any]`: 被装饰的同步函数
* **返回**
* `Callable[..., Awaitable[Any]]`
## _class_ `DataclassEncoder`
基类:`json.encoder.JSONEncoder`
* **说明**
在JSON序列化 `Message` (List[Dataclass]) 时使用的 `JSONEncoder`

View File

@ -0,0 +1,38 @@
# 概览
:::tip 提示
如果在阅读本文档时遇到难以理解的词汇,请随时查阅 [术语表](../glossary.md) 或使用 [Google 搜索](https://www.google.com/)。
:::
:::tip 提示
初次使用时可能会觉得这里的概览过于枯燥,可以先简单略读之后直接前往 [安装](./installation.md) 查看安装方法,并进行后续的基础使用教程。
:::
NoneBot2 是一个可扩展的 Python 异步机器人框架,它会对机器人收到的消息进行解析和处理,并以插件化的形式,分发给消息所对应的命令处理器和自然语言处理器,来完成具体的功能。
除了起到解析消息的作用NoneBot 还为插件提供了大量实用的预设操作和权限控制机制,尤其对于命令处理器,它更是提供了完善且易用的会话机制和内部调用机制,以分别适应命令的连续交互和插件内部功能复用等需求。
目前 NoneBot2 在 [FastAPI](https://fastapi.tiangolo.com/) 的基础上封装了与 [CQHTTP(OneBot) 协议](http://cqhttp.cc/)插件的网络交互。
得益于 Python 的 [asyncio](https://docs.python.org/3/library/asyncio.html) 机制NoneBot 处理消息的吞吐量有了很大的保障,再配合 WebSocket 通信方式也是最建议的通信方式NoneBot 的性能可以达到 HTTP 通信方式的两倍以上,相较于传统同步 I/O 的 HTTP 通信,更是有质的飞跃。
需要注意的是NoneBot 仅支持 Python 3.7+ 及 CQHTTP(OneBot) 插件 v11+。
## 它如何工作?
NoneBot 的运行离不开 酷 Q 和 CQHTTP 插件。酷 Q 扮演着「无头 QQ 客户端」的角色,它进行实际的消息、通知、请求的接收和发送,当 酷 Q 收到消息时,它将这个消息包装为一个事件(通知和请求同理),并通过它自己的插件机制将事件传送给 CQHTTP 插件,后者再根据其配置中的 `post_url``ws_reverse_url` 等项来将事件发送至 NoneBot。
在 NoneBot 收到事件前,它底层的 aiocqhttp 实际已经先看到了事件aiocqhttp 根据事件的类型信息,通知到 NoneBot 的相应函数。特别地,对于消息类型的事件,还将消息内容转换成了 `aiocqhttp.message.Message` 类型,以便处理。
NoneBot 的事件处理函数收到通知后对于不同类型的事件再做相应的预处理和解析然后调用对应的插件并向其提供适合此类事件的会话Session对象。NoneBot 插件的编写者要做的,就是利用 Session 对象中提供的数据,在插件的处理函数中实现所需的功能。
## 特色
- 提供直观的测试前端
- 提供使用简易的脚手架
- 基于异步 I/O
- 同时支持 HTTP 和反向 WebSocket 通信方式
- 支持多个机器人账号负载均衡
- 提供直观的交互式会话接口
- 提供可自定义的权限控制机制
- 多种方式渲染要发送的消息内容,使对话足够自然

View File

@ -0,0 +1,68 @@
# 基本配置
到目前为止我们还在使用 NoneBot 的默认行为,在开始编写自己的插件之前,我们先尝试在配置文件上动动手脚,让 NoneBot 表现出不同的行为。
在上一章节中,我们创建了默认的项目结构,其中 `.env`, `.env.*` 均为项目的配置文件,下面将介绍几种 NoneBot 配置方式。
:::danger 警告
请勿将敏感信息写入配置文件并提交至开源仓库!
:::
## .env 文件
NoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找变量 `ENVIRONMENT` (大小写不敏感),默认值为 `prod`
这将引导 NoneBot 从系统环境变量或者 `.env.{ENVIRONMENT}` 文件中进一步加载具体配置。
现在,我们在 `.env` 文件中写入当前环境信息
```bash
# .env
ENVIRONMENT=dev
```
## .env.\* 文件
详细配置文件,使用 [pydantic](https://pydantic-docs.helpmanual.io/) 加载配置。在 NoneBot 初始化时可以指定忽略 `.env` 中的环境信息转而加载某个配置文件: `nonebot.init(_env_file=".env.dev")`
:::warning 提示
由于 `pydantic` 使用 JSON 加载配置项,请确保配置项值为 JSON 能够解析的数据。如果 JSON 解析失败将作为字符串处理。
:::
示例及说明:
```bash
HOST=0.0.0.0 # 配置 NoneBot 监听的 IP/主机名
PORT=8080 # 配置 NoneBot 监听的端口
DEBUG=true # 开启 debug 模式 **请勿在生产环境开启**
SUPERUSERS=["123456789", "987654321"] # 配置 NoneBot 超级用户
NICKNAME=["awesome", "bot"] # 配置机器人的昵称
COMMAND_START=["/", ""] # 配置命令起始字符
COMMAND_SEP=["."] # 配置命令分割字符
# Custom Configs
CUSTOM_CONFIG1="config in env file"
CUSTOM_CONFIG2= # 留空则从系统环境变量读取,如不存在则为空字符串
```
详细的配置项参考 [Config Reference](../api/config.md) 。
## bot.py 文件
配置项也可以在 NoneBot 初始化时传入。此处可以传入任意合法 Python 变量。当然也可以在初始化完成后修改或新增。
示例:
```python
# bot.py
import nonebot
nonebot.init(custom_config3="config on init")
config = nonebot.get_driver().config
config.custom_config3 = "changed after init"
config.custom_config4 = "new config after init"
```
## 优先级
`bot.py init` > `env file` > `system env`

View File

@ -0,0 +1,55 @@
# 创建一个完整的项目
上一章中我们已经运行了一个最小的 NoneBot 实例,在这一章,我们将从零开始一个完整的项目。
## 目录结构
首先,我们可以使用 `nb-cli` 或者自行创建项目目录:
```bash
pip install nonebot2[cli]
# pip install nb-cli
nb create
```
这将创建默认的目录结构
<!-- prettier-ignore-start -->
:::vue
AweSome-Bot
├── `awesome_bot` _(**或是 src**)_
│ └── `plugins`
├── `.env` _(**可选的**)_
├── `.env.dev` _(**可选的**)_
├── `.env.prod` _(**可选的**)_
├── .gitignore
├── `bot.py`
├── docker-compose.yml
├── Dockerfile
├── `pyproject.toml`
└── README.md
:::
<!-- prettier-ignore-end -->
- `awesome_bot/plugins``src/plugins`: 用于存放编写的 bot 插件
- `.env`, `.env.dev`, `.env.prod`: 各环境配置文件
- `bot.py`: bot 入口文件
- `pyproject.toml`: 项目依赖管理文件,默认使用 [poetry](https://python-poetry.org/)
## 启动 Bot
如果你使用 `nb-cli`
```bash
nb run [--file=bot.py] [--app=app]
```
或者使用
```bash
python bot.py
```
:::tip 提示
如果在 bot 入口文件内定义了 asgi server `nb-cli` 将会为你启动**冷重载模式**
:::

View File

@ -0,0 +1,146 @@
# 开始使用
一切都安装成功后,你就已经做好了进行简单配置以运行一个最小的 NoneBot 实例的准备。
## 最小实例
使用你最熟悉的编辑器或 IDE创建一个名为 `bot.py` 的文件,内容如下:
```python{3,4,7}
import nonebot
nonebot.init()
nonebot.load_builtin_plugins()
if __name__ == "__main__":
nonebot.run()
```
这几行高亮代码将依次:
1. 使用默认配置初始化 NoneBot 包
2. 加载 NoneBot 内置的插件
3. 在地址 `127.0.0.1:8080` 运行 NoneBot
在命令行使用如下命令即可运行这个 NoneBot 实例:
```bash
python bot.py
```
运行后会产生如下日志:
```default
09-14 21:02:00 [INFO] nonebot | Succeeded to import "nonebot.plugins.base"
09-14 21:02:00 [INFO] nonebot | Running NoneBot...
09-14 21:02:00 [INFO] uvicorn | Started server process [1234]
09-14 21:02:00 [INFO] uvicorn | Waiting for application startup.
09-14 21:02:00 [INFO] nonebot | Scheduler Started
09-14 21:02:00 [INFO] uvicorn | Application startup complete.
09-14 21:02:00 [INFO] uvicorn | Uvicorn running on http://127.0.0.1:8080 (Press CTRL+C to quit)
```
## 配置 QQ 协议端
单纯运行 NoneBot 实例并不会产生任何效果,因为此刻 QQ 这边还不知道 NoneBot 的存在,也就无法把消息发送给它,因此现在需要使用一个无头 QQ 来把消息等事件上报给 NoneBot。
目前支持的协议有:
- [OneBot(CQHTTP)](https://github.com/howmanybots/onebot)
QQ 协议端举例:
- [Mirai](https://github.com/mamoe/mirai) + [cqhttp-mirai](https://github.com/yyuueexxiinngg/cqhttp-mirai)
- [cqhttp-mirai-embedded](https://github.com/yyuueexxiinngg/cqhttp-mirai/tree/embedded)
- [Mirai](https://github.com/mamoe/mirai) + [Mirai Native](https://github.com/iTXTech/mirai-native) + [CQHTTP](https://github.com/richardchien/coolq-http-api)
- [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) (基于 [MiraiGo](https://github.com/Mrs4s/MiraiGo))
- [OICQ-http-api](https://github.com/takayama-lily/onebot) (基于 [OICQ](https://github.com/takayama-lily/oicq))
这里以 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 为例
1. 下载 go-cqhttp 对应平台的 release 文件
2. 双击 exe 文件或者使用 `./go-cqhttp` 启动
3. 生成默认配置文件并修改默认配置
```json{2,3,30-31}
{
"uin": 你的QQ号,
"password": "你的密码",
"encrypt_password": false,
"password_encrypted": "",
"enable_db": true,
"access_token": "",
"relogin": {
"enabled": true,
"relogin_delay": 3,
"max_relogin_times": 0
},
"ignore_invalid_cqcode": false,
"force_fragmented": true,
"heartbeat_interval": 0,
"http_config": {
"enabled": false,
"host": "0.0.0.0",
"port": 5700,
"timeout": 0,
"post_urls": {}
},
"ws_config": {
"enabled": false,
"host": "0.0.0.0",
"port": 6700
},
"ws_reverse_servers": [
{
"enabled": true,
"reverse_url": "ws://127.0.0.1:8080/cqhttp/ws",
"reverse_api_url": "",
"reverse_event_url": "",
"reverse_reconnect_interval": 3000
}
],
"post_message_format": "string",
"debug": false,
"log_level": ""
}
```
其中 `ws://127.0.0.1:8080/cqhttp/ws` 中的 `127.0.0.1``8080` 应分别对应 nonebot 配置的 HOST 和 PORT
## 历史性的第一次对话
一旦新的配置文件正确生效之后NoneBot 所在的控制台(如果正在运行的话)应该会输出类似下面的内容(两条访问日志):
```default
09-14 21:31:16 [INFO] uvicorn | ('127.0.0.1', 12345) - "WebSocket /cqhttp/ws" [accepted]
09-14 21:31:16 [INFO] nonebot | WebSocket Connection from CQHTTP Bot 你的QQ号 Accepted!
```
这表示 QQ 协议端已经成功地使用 CQHTTP 协议连接上了 NoneBot。
:::warning 注意
如果到这一步你没有看到上面这样的成功日志CQHTTP 的日志中在不断地重连或无反应,请注意检查配置中的 IP 和端口是否确实可以访问。比较常见的出错点包括:
- NoneBot 监听 `0.0.0.0`,然后在 CQHTTP 配置中填了 `ws://0.0.0.0:8080/cqhttp/ws`
- 在 Docker 容器内运行 CQHTTP并通过 `127.0.0.1` 访问宿主机上的 NoneBot
- 想从公网访问,但没有修改云服务商的安全组策略或系统防火墙
- NoneBot 所监听的端口存在冲突,已被其它程序占用
- 弄混了 NoneBot 的 `host``port` 参数与 CQHTTP 配置中的 `host``port` 参数
- 使用了 `ws_reverse_api_url``ws_reverse_event_url` 而非 universal client
- `ws://` 错填为 `http://`
- CQHTTP 或 NoneBot 启动时遭到外星武器干扰
请尝试重启 CQHTTP、重启 NoneBot、更换端口、修改防火墙、重启系统、仔细阅读前面的文档及提示、更新 CQHTTP 和 NoneBot 到最新版本等方式来解决。
:::
现在,尝试向你的 QQ 机器人账号发送如下内容:
```default
/say 你好,世界
```
到这里如果一切 OK你应该会收到机器人给你回复了 `你好,世界`。这一历史性的对话标志着你已经成功地运行了一个 NoneBot 的最小实例,开始了编写更强大的 QQ 机器人的创意之旅!
<ClientOnly>
<Messenger :messages="[{ position: 'right', msg: '/say 你好,世界' }, { position: 'left', msg: '你好,世界' }]"/>
</ClientOnly>

View File

@ -0,0 +1,73 @@
# 安装
## NoneBot
:::warning 注意
请确保你的 Python 版本 >= 3.7。
:::
```bash
pip install nonebot2
```
如果你需要使用最新的(可能尚未发布的)特性,可以克隆 Git 仓库后手动安装:
```bash
git clone https://github.com/nonebot/nonebot2.git
cd nonebot2
poetry install --no-dev # 推荐
pip install . # 不推荐
```
## 额外依赖
### APScheduler
A task scheduling library for Python.
可用于计划任务,后台执行任务等
```bash
pip install nonebot2[scheduler]
poetry add nonebot2[scheduler]
```
[View On GitHub](https://github.com/agronholm/apscheduler)
### NoneBot-Test
A test frontend for nonebot2.
通过前端展示 nonebot 已加载的插件以及运行状态,同时可以用于模拟发送事件测试机器人
```bash
pip install nonebot2[test]
poetry add nonebot2[test]
```
[View On GitHub](https://github.com/nonebot/nonebot-test)
### CLI
CLI for nonebot2.
一个多功能脚手架
```bash
pip install nonebot2[cli]
poetry add nonebot2[cli]
```
[View On GitHub](https://github.com/yanyongyu/nb-cli)
### 我全都要
```bash
pip install nonebot2[full]
poetry add nonebot2[full]
```
```bash
pip install nonebot2[cli,scheduler]
poetry add nonebot2[cli,scheduler]
```

View File

@ -0,0 +1,290 @@
# 编写插件
本章将以一个天气查询插件为例,教学如何编写自己的命令。
## 加载插件
在 [创建一个完整的项目](creating-a-project) 一章节中,我们已经创建了插件目录 `awesome_bot/plugins`,现在我们在机器人入口文件中加载它。当然,你也可以单独加载一个插件。
:::tip 提示
加载插件目录时,目录下以 `_` 下划线开头的插件将不会被加载!
:::
`bot.py` 文件中添加以下行:
```python{5,7}
import nonebot
nonebot.init()
# 加载单独的一个插件参数为合法的python包名
nonebot.load_plugin("nonebot.plugins.base")
# 加载插件目录,该目录下为各插件,以下划线开头的插件将不会被加载
nonebot.load_plugins("awesome_bot/plugins")
app = nonebot.get_asgi()
if __name__ == "__main__":
nonebot.run()
```
尝试运行 `nb run` 或者 `python bot.py`,可以看到日志输出了类似如下内容:
```plain
09-19 21:51:59 [INFO] nonebot | Succeeded to import "nonebot.plugins.base"
09-19 21:51:59 [INFO] nonebot | Succeeded to import "plugin_in_folder"
```
## 创建插件
现在我们已经有了一个空的插件目录,我们可以开始创建插件了!插件有两种形式
### 单文件形式
在插件目录下创建名为 `weather.py` 的 Python 文件,暂时留空,此时目录结构如下:
<!-- prettier-ignore-start -->
:::vue
AweSome-Bot
├── awesome_bot
│ └── plugins
│ └── `weather.py`
├── .env
├── .env.dev
├── .env.prod
├── .gitignore
├── bot.py
├── docker-compose.yml
├── Dockerfile
├── pyproject.toml
└── README.md
:::
<!-- prettier-ignore-end -->
这个时候它已经可以被称为一个插件了,尽管它还什么都没做。
### 包形式
在插件目录下创建文件夹 `weather`,并在该文件夹下创建文件 `__init__.py`,此时目录结构如下:
<!-- prettier-ignore-start -->
:::vue
AweSome-Bot
├── awesome_bot
│ └── plugins
│ └── `weather`
│ └── `__init__.py`
├── .env
├── .env.dev
├── .env.prod
├── .gitignore
├── bot.py
├── docker-compose.yml
├── Dockerfile
├── pyproject.toml
└── README.md
:::
<!-- prettier-ignore-end -->
这个时候 `weather` 就是一个合法的 Python 包了,同时也是合法的 NoneBot 插件,插件内容可以在 `__init__.py` 中编写。
## 编写真正的内容
好了,现在插件已经可以正确加载,我们可以开始编写命令的实际代码了。在 `weather.py` 中添加如下代码:
```python
from nonebot import on_command
from nonebot.rule import to_me
from nonebot.adapters.cqhttp import Bot, Event
weather = on_command("天气", rule=to_me(), priority=5)
@weather.handle()
async def handle_first_receive(bot: Bot, event: Event, state: dict):
args = str(event.message).strip() # 首次发送命令时跟随的参数,例:/天气 上海则args为上海
if args:
state["city"] = args # 如果用户发送了参数则直接赋值
@weather.got("city", prompt="你想查询哪个城市的天气呢?")
async def handle_city(bot: Bot, event: Event, state: dict):
city = state["city"]
if city not in ["上海", "北京"]:
await weather.reject("你想查询的城市暂不支持,请重新输入!")
city_weather = await get_weather(city)
await weather.finish(city_weather)
async def get_weather(city: str):
return f"{city}的天气是..."
```
为了简单起见,我们在这里的例子中没有接入真实的天气数据,但要接入也非常简单,你可以使用中国天气网、和风天气等网站提供的 API。
下面我们来说明这段代码是如何工作的。
:::tip 提示
从这里开始,你需要对 Python 的 asyncio 编程有所了解,因为 NoneBot 是完全基于 asyncio 的,具体可以参考 [廖雪峰的 Python 教程](https://www.liaoxuefeng.com/wiki/1016959663602400/1017959540289152)
:::
### 注册一个 [事件响应器](../api/matcher.md)
```python{4}
from nonebot import on_command
from nonebot.rule import to_me
from nonebot.permission import Permission
weather = on_command("天气", rule=to_me(), permission=Permission(), priority=5)
```
在上方代码中,我们注册了一个事件响应器 `Matcher`,它由几个部分组成:
1. `on_command` 注册一个消息类型的命令处理器
2. `"天气"` 指定 command 参数 - 命令名
3. `rule` 补充事件响应器的匹配规则
4. `priority` 事件响应器优先级
5. `block` 是否阻止事件传递
其他详细配置可以参考 API 文档,下面我们详细说明各个部分:
#### 事件响应器类型 type
事件响应器类型其实就是对应 `Event.type` NoneBot 提供了一个基础类型事件响应器 `on()` 以及一些内置的事件响应器。
- `on("事件类型")`: 基础事件响应器,第一个参数为事件类型,空字符串表示不限
- `on_metaevent()` ~ `on("meta_event")`: 元事件响应器
- `on_message()` ~ `on("message")`: 消息事件响应器
- `on_request()` ~ `on("request")`: 请求事件响应器
- `on_notice()` ~ `on("notice")`: 通知事件响应器
- `on_startswith(str)` ~ `on("message", startswith(str))`: 消息开头匹配处理器
- `on_endswith(str)` ~ `on("message", endswith(str))`: 消息结尾匹配处理器
- `on_command(str|tuple)` ~ `on("message", command(str|tuple))`: 命令处理器
- `on_regax(pattern_str)` ~ `on("message", regax(pattern_str))`: 正则匹配处理器
#### 匹配规则 rule
事件响应器的匹配规则即 `Rule`,由非负个 `RuleChecker` 组成,当所有 `RuleChecker` 返回 `True` 时匹配成功。这些 `RuleChecker` 的形式如下:
```python
async def check(bot: Bot, event: Event, state: dict) -> bool:
return True
def check(bot: Bot, event: Event, state: dict) -> bool:
return True
```
`Rule``RuleChecker` 之间可以使用 `与 &` 互相组合:
```python
from nonebot.rule import Rule
Rule(async_checker1) & sync_checker & async_checker2
```
:::danger 警告
`Rule(*checkers)` 只接受 async function或使用 `nonebot.utils.run_sync` 自行包裹 sync function。在使用 `与 &`NoneBot 会自动包裹 sync function
:::
#### 优先级 priority
事件响应器的优先级代表事件响应器的执行顺序,同一优先级的事件响应器会 **同时执行!**
:::tip 提示
使用 `nonebot-test` 可以看到当前所有事件响应器的执行流程,有助理解事件响应流程!
```bash
pip install nonebot2[test]
```
:::
#### 阻断 block
当有任意事件响应器发出了阻止事件传递信号时,该事件将不再会传递给下一优先级,直接结束处理。
NoneBot 内置的事件响应器中,所有 `message` 类的事件响应器默认会阻断事件传递,其他则不会。
### 编写事件处理函数 [Handler](../api/typing.md#handler)
```python{1,2,8,9}
@weather.handle()
async def handle_first_receive(bot: Bot, event: Event, state: dict):
args = str(event.message).strip() # 首次发送命令时跟随的参数,例:/天气 上海则args为上海
if args:
state["city"] = args # 如果用户发送了参数则直接赋值
@weather.got("city", prompt="你想查询哪个城市的天气呢?")
async def handle_city(bot: Bot, event: Event, state: dict):
city = state["city"]
if city not in ["上海", "北京"]:
await weather.reject("你想查询的城市暂不支持,请重新输入!")
city_weather = await get_weather(city)
await weather.finish(city_weather)
```
在上面的代码中,我们给 `weather` 事件响应器添加了两个事件处理函数:`handle_first_receive`, `handle_city`
其中有几个要点,我们一一解释:
#### 添加一个事件处理函数
在事件响应器响应事件时,事件处理函数会依次顺序执行,也就是说,与添加顺序一致。
我们可以使用 `@matcher.handle()` 装饰器来简单地为该事件响应器添加一个处理函数。
同时NoneBot 内置了几种添加事件处理函数方式以方便处理:
- `@matcher.receive()`: 指示 NoneBot 接收一条新的用户消息以继续执行后续处理函数。
- `@matcher.got(key, [prompt="请输入key"], [args_parser=function])`: 指示 NoneBot 当 `state` 中不存在 `key` 时向用户发送 `prompt` 等待用户回复并赋值给 `state[key]`
这些装饰器可以套娃使用!例如:
```python
@matcher.got("key1")
@matcher.got("key2")
async def handle(bot: Bot, event: Event, state: dict):
pass
```
#### 事件处理函数参数
事件处理函数类型为 `Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]`
参数分别为:
1. [nonebot.typing.Bot](../api/typing.md#bot): 即事件上报连接对应的 Bot 对象,为 BaseBot 的子类。特别注意,此处的类型注释可以替换为指定的 Bot 类型,例如:`nonebot.adapters.cqhttp.Bot`,只有在上报事件的 Bot 类型与类型注释相符时才会执行该处理函数!可用于多平台进行不同的处理。
2. [nonebot.typing.Event](../api/typing.md#event): 即上报事件对象,可以获取到上报的所有信息。
3. `state`: 状态字典,可以存储任意的信息
#### 处理事件
在事件处理函数中,我们只需要对 `event` 做出相应的处理,存入状态字典 `state` 中,或者向用户发送消息、调用某个机器人 API 等等。
在 NoneBot 中,提供了几种特殊的处理函数:
##### `@matcher.args_parser`
这是一个装饰器,装饰一个函数来使它成为参数的默认解析函数,当使用 `matcher.got(xxx, [args_parser])` 获取到一条消息时,会运行 `matcher.got``args_parser` ,如果不存在则运行 `@matcher.args_parser`
##### `matcher.pause`
这个函数用于结束当前事件处理函数,强制接收一条新的消息再运行**下一个消息处理函数**。
##### `matcher.reject`
这个函数用于结束当前事件处理函数,强制接收一条新的消息再**再次运行当前消息处理函数**。
##### `matcher.finish`
这个函数用于直接结束当前事件处理。
以上三个函数都拥有一个参数 `prompt`,用于向用户发送一条消息。
## 结语
至此,相信你已经能够写出一个基础的插件了,更多的用法将会在 进阶 部分进行介绍,这里给出几个小提示:
- 请千万注意事件处理器的优先级设定
- 在匹配规则中请勿使用耗时极长的函数
- 同一个用户可以跨群(私聊)继续他的事件处理(除非做出权限限制,将在后续介绍)

View File

@ -0,0 +1,88 @@
{
"locales": {
"/": {
"label": "简体中文",
"selectText": "Languages",
"editLinkText": "在 GitHub 上编辑此页",
"lastUpdated": "上次更新",
"nav": [
{
"text": "主页",
"link": "/"
},
{
"text": "指南",
"link": "/guide/"
},
{
"text": "API",
"link": "/api/"
}
],
"sidebarDepth": 2,
"sidebar": {
"/guide/": [
{
"title": "指南",
"path": "",
"collapsable": false,
"sidebar": "auto",
"children": [
"",
"installation",
"getting-started",
"creating-a-project",
"basic-configuration",
"writing-a-plugin"
]
}
],
"/api/": [
{
"title": "NoneBot Api Reference",
"path": "",
"collapsable": false,
"children": [
{
"title": "nonebot 模块",
"path": "nonebot"
},
{
"title": "nonebot.typing 模块",
"path": "typing"
},
{
"title": "nonebot.config 模块",
"path": "config"
},
{
"title": "nonebot.sched 模块",
"path": "sched"
},
{
"title": "nonebot.log 模块",
"path": "log"
},
{
"title": "nonebot.rule 模块",
"path": "rule"
},
{
"title": "nonebot.permission 模块",
"path": "permission"
},
{
"title": "nonebot.utils 模块",
"path": "utils"
},
{
"title": "nonebot.exception 模块",
"path": "exception"
}
]
}
]
}
}
}
}

View File

@ -0,0 +1,216 @@
<template>
<div class="qq-chat">
<v-app
><v-main>
<v-card class="elevation-6">
<v-toolbar color="primary" dark dense flat>
<v-row no-gutters>
<v-col>
<v-row no-gutters justify="space-between">
<v-col cols="auto">
<v-icon small>fa-chevron-left</v-icon>
</v-col>
<v-col cols="auto">
<h3>🔥</h3>
</v-col>
</v-row>
</v-col>
<v-col cols="auto">
<h3 class="white--text">NoneBot</h3>
</v-col>
<v-col class="text-right">
<v-icon small>fa-user</v-icon>
</v-col>
</v-row>
</v-toolbar>
<v-container fluid ref="chat" class="chat chat-bg">
<template v-for="(item, index) in messages">
<v-row
v-if="item.position === 'right'"
justify="end"
:key="index"
class="message wow animate__fadeInRight"
data-wow-duration="0.7s"
>
<div
class="message-box"
v-html="
item.msg.replace(/\n/g, '<br/>').replace(/ /g, '&nbsp;')
"
></div>
<v-avatar color="blue lighten-2" size="36">
<v-icon small>fa-user</v-icon>
</v-avatar>
</v-row>
<v-row
v-else-if="item.position === 'left'"
justify="start"
:key="index"
class="message wow animate__fadeInLeft"
data-wow-duration="0.7s"
>
<v-avatar color="transparent" size="36">
<v-img src="/logo.png"></v-img>
</v-avatar>
<div
class="message-box"
v-html="
item.msg.replace(/\n/g, '<br/>').replace(/ /g, '&nbsp;')
"
></div>
</v-row>
<v-row
v-else
justify="center"
:key="index"
class="notify mt-1 wow animate__fadeIn"
data-wow-duration="0.7s"
>
<div class="notify-box">
<span style="display: inline; white-space: nowrap">
<v-icon x-small color="blue" left>fa-info-circle</v-icon>
</span>
<span
v-html="
item.msg.replace(/\n/g, '<br/>').replace(/ /g, '&nbsp;')
"
></span>
</div>
</v-row>
</template>
</v-container>
<v-container fluid class="chat-bg py-0">
<v-row dense class="mx-0">
<v-col>
<v-text-field
dense
solo
hide-details
height="28px"
></v-text-field>
</v-col>
<v-col cols="auto">
<v-btn
style="font-size: 0.8rem"
color="primary"
small
rounded
depressed
>发送</v-btn
>
</v-col>
</v-row>
<v-row class="text-center" no-gutters>
<v-col class="pa-1" cols="2">
<v-icon small>fa-microphone</v-icon>
</v-col>
<v-col class="pa-1" cols="2">
<v-icon small>fa-image</v-icon>
</v-col>
<v-col class="pa-1" cols="2">
<v-icon small>fa-camera</v-icon>
</v-col>
<v-col class="pa-1" cols="2">
<v-icon small>fa-wallet</v-icon>
</v-col>
<v-col class="pa-1" cols="2">
<v-icon small>fa-smile-wink</v-icon>
</v-col>
<v-col class="pa-1" cols="2">
<v-icon small>fa-plus-circle</v-icon>
</v-col>
</v-row>
</v-container>
</v-card>
</v-main>
</v-app>
</div>
</template>
<script>
import { WOW } from "wowjs";
import "animate.css/animate.min.css";
export default {
name: "Messenger",
props: {
messages: {
type: Array,
default: () => []
}
},
data: () => ({
wow: null
}),
methods: {
initWOW: function() {
this.wow = new WOW({
noxClass: "wow",
animateClass: "animate__animated",
offset: 0,
mobile: true,
live: true
});
this.wow.init();
}
},
mounted() {
this.initWOW();
}
};
</script>
<style scoped>
.wow {
visibility: hidden;
}
.chat {
min-height: 150px;
}
.chat-bg {
background-color: #f3f6f9;
}
.message {
position: relative;
margin: 0;
}
.message .message-box {
position: relative;
width: fit-content;
max-width: 55%;
border-radius: 0.5rem;
padding: 0.6rem 0.8rem;
margin: 0.4rem 0.8rem;
background-color: #fff;
}
.message .message-box::after {
content: "";
position: absolute;
right: 100%;
top: 0;
width: 8px;
height: 12px;
color: #fff;
border: 0 solid transparent;
border-bottom: 7px solid;
border-radius: 0 0 0 8px;
}
.message.justify-end .message-box::after {
left: 100%;
right: auto;
border-radius: 0 0 8px 0;
}
.notify {
position: relative;
}
.notify .notify-box {
max-width: 70%;
background: #e0e0e0;
border-radius: 10px;
padding: 5px 12px;
font-size: 12px;
}
</style>

View File

@ -20,6 +20,14 @@ module.exports = context => ({
[ [
"meta", "meta",
{ name: "apple-mobile-web-app-status-bar-style", content: "black" } { name: "apple-mobile-web-app-status-bar-style", content: "black" }
],
[
"link",
{
rel: "stylesheet",
href:
"https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5/css/all.min.css"
}
] ]
], ],
locales: { locales: {
@ -47,8 +55,29 @@ module.exports = context => ({
selectText: "Languages", selectText: "Languages",
editLinkText: "在 GitHub 上编辑此页", editLinkText: "在 GitHub 上编辑此页",
lastUpdated: "上次更新", lastUpdated: "上次更新",
nav: [{ text: "API", link: "/api/" }], nav: [
{ text: "主页", link: "/" },
{ text: "指南", link: "/guide/" },
{ text: "API", link: "/api/" }
],
sidebarDepth: 2,
sidebar: { sidebar: {
"/guide/": [
{
title: "指南",
path: "",
collapsable: false,
sidebar: "auto",
children: [
"",
"installation",
"getting-started",
"creating-a-project",
"basic-configuration",
"writing-a-plugin"
]
}
],
"/api/": [ "/api/": [
{ {
title: "NoneBot Api Reference", title: "NoneBot Api Reference",
@ -63,17 +92,33 @@ module.exports = context => ({
title: "nonebot.typing 模块", title: "nonebot.typing 模块",
path: "typing" path: "typing"
}, },
{
title: "nonebot.config 模块",
path: "config"
},
{
title: "nonebot.sched 模块",
path: "sched"
},
{ {
title: "nonebot.log 模块", title: "nonebot.log 模块",
path: "log" path: "log"
}, },
{ {
title: "nonebot.exception 模块", title: "nonebot.rule 模块",
path: "exception" path: "rule"
}, },
{ {
title: "nonebot.config 模块", title: "nonebot.permission 模块",
path: "config" path: "permission"
},
{
title: "nonebot.utils 模块",
path: "utils"
},
{
title: "nonebot.exception 模块",
path: "exception"
} }
] ]
} }
@ -95,6 +140,14 @@ module.exports = context => ({
console.log(`Created version ${version} in ${versionDestPath}`); console.log(`Created version ${version} in ${versionDestPath}`);
} }
} }
],
[
"container",
{
type: "vue",
before: '<pre class="vue-container"><code>',
after: "</code></pre>"
}
] ]
] ]
}); });

View File

@ -4,12 +4,25 @@
* https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements * https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements
*/ */
import Vuetify from "vuetify";
import "vuetify/dist/vuetify.min.css";
export default ({ export default ({
Vue, // the version of Vue being used in the VuePress app Vue, // the version of Vue being used in the VuePress app
options, // the options for the root Vue instance options, // the options for the root Vue instance
router, // the router instance for the app router, // the router instance for the app
siteData // site metadata siteData // site metadata
}) => { }) => {
Vue.use(Vuetify);
options.vuetify = new Vuetify({
icons: {
iconfont: "fa",
values: {
//
}
}
});
if (typeof process === "undefined" || process.env.VUE_ENV !== "server") { if (typeof process === "undefined" || process.env.VUE_ENV !== "server") {
router.onReady(() => { router.onReady(() => {
const { app } = router; const { app } = router;
@ -18,7 +31,7 @@ export default ({
setTimeout(() => { setTimeout(() => {
const { hash } = document.location; const { hash } = document.location;
if (hash.length > 1) { if (hash.length > 1) {
const id = hash.substring(1); const id = decodeURI(hash.substring(1));
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) element.scrollIntoView(); if (element) element.scrollIntoView();
} }

View File

@ -0,0 +1,3 @@
[
"2.0.0a1"
]

View File

@ -10,7 +10,22 @@
* [nonebot.typing](typing.html) * [nonebot.typing](typing.html)
* [nonebot.config](config.html)
* [nonebot.sched](sched.html)
* [nonebot.log](log.html) * [nonebot.log](log.html)
* [nonebot.config](config.html) * [nonebot.rule](rule.html)
* [nonebot.permission](permission.html)
* [nonebot.utils](utils.html)
* [nonebot.exception](exception.html)

View File

@ -67,7 +67,7 @@ NoneBot 运行所使用的 `Driver` 。继承自 `nonebot.driver.BaseDriver` 。
* 说明: * 说明:
NoneBot 的 HTTP 和 WebSocket 服务端监听的 IP主机名。 NoneBot 的 HTTP 和 WebSocket 服务端监听的 IP/主机名。
### `port` ### `port`
@ -83,27 +83,6 @@ NoneBot 的 HTTP 和 WebSocket 服务端监听的 IP主机名。
NoneBot 的 HTTP 和 WebSocket 服务端监听的端口。 NoneBot 的 HTTP 和 WebSocket 服务端监听的端口。
### `secret`
* 类型: `Optional[str]`
* 默认值: `None`
* 说明:
上报连接 NoneBot 所需的密钥。
* 示例:
```http
POST /cqhttp/ HTTP/1.1
Authorization: Bearer kSLuTF2GC2Q4q4ugm3
```
### `debug` ### `debug`
@ -132,7 +111,7 @@ Authorization: Bearer kSLuTF2GC2Q4q4ugm3
* 示例: * 示例:
```plain ```default
API_ROOT={"123456": "http://127.0.0.1:5700"} API_ROOT={"123456": "http://127.0.0.1:5700"}
``` ```
@ -143,7 +122,7 @@ API_ROOT={"123456": "http://127.0.0.1:5700"}
* 类型: `Optional[float]` * 类型: `Optional[float]`
* 默认值: `60.` * 默认值: `30.`
* 说明: * 说明:
@ -160,7 +139,36 @@ API 请求超时时间,单位: 秒。
* 说明: * 说明:
API 请求所需密钥,会在调用 API 时在请求头中携带。 API 请求以及上报所需密钥,在请求头中携带。
* 示例:
```http
POST /cqhttp/ HTTP/1.1
Authorization: Bearer kSLuTF2GC2Q4q4ugm3
```
### `secret`
* 类型: `Optional[str]`
* 默认值: `None`
* 说明:
HTTP POST 形式上报所需签名,在请求头中携带。
* 示例:
```http
POST /cqhttp/ HTTP/1.1
X-Signature: sha1=f9ddd4863ace61e64f462d41ca311e3d2c1176e2
```
### `superusers` ### `superusers`
@ -178,7 +186,7 @@ API 请求所需密钥,会在调用 API 时在请求头中携带。
* 示例: * 示例:
```plain ```default
SUPER_USERS=[12345789] SUPER_USERS=[12345789]
``` ```
@ -237,8 +245,21 @@ SUPER_USERS=[12345789]
* 示例: * 示例:
```plain ```default
SESSION_EXPIRE_TIMEOUT=120 # 单位: 秒 SESSION_EXPIRE_TIMEOUT=120 # 单位: 秒
SESSION_EXPIRE_TIMEOUT=[DD ][HH:MM]SS[.ffffff] SESSION_EXPIRE_TIMEOUT=[DD ][HH:MM]SS[.ffffff]
SESSION_EXPIRE_TIMEOUT=P[DD]DT[HH]H[MM]M[SS]S # ISO 8601 SESSION_EXPIRE_TIMEOUT=P[DD]DT[HH]H[MM]M[SS]S # ISO 8601
``` ```
### `apscheduler_config`
* 类型: `dict`
* 默认值: `{"apscheduler.timezone": "Asia/Shanghai"}`
* 说明:
APScheduler 的配置对象,见 [Configuring the Scheduler](https://apscheduler.readthedocs.io/en/latest/userguide.html#configuring-the-scheduler)

View File

@ -39,8 +39,17 @@ NoneBot 使用 [loguru](https://github.com/Delgan/loguru) 来记录日志信息
```python ```python
from nonebot.log import logger from nonebot.log import logger
# 也可以这样
import logging
logger = logging.getLogger("nonebot")
``` ```
## _class_ `LoguruHandler`
基类:`logging.Handler`
### `emit(record)`
Do whatever it takes to actually log the specified logging record.
This version is intended to be implemented by subclasses and so
raises a NotImplementedError.

View File

@ -156,7 +156,7 @@ bots = nonebot.get_bots()
* **返回** * **返回**
* None * `None`
@ -196,7 +196,7 @@ nonebot.init(database=Database(...))
* **返回** * **返回**
* None * `None`

121
docs/api/permission.md Normal file
View File

@ -0,0 +1,121 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.permission 模块
## 权限
每个 `Matcher` 拥有一个 `Permission` ,其中是 **异步** `PermissionChecker` 的集合,只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
:::tip 提示
`PermissionChecker` 既可以是 async function 也可以是 sync function
:::
## `MESSAGE`
* **说明**: 匹配任意 `message` 类型事件,仅在需要同时捕获不同类型事件时使用。优先使用 message type 的 Matcher。
## `NOTICE`
* **说明**: 匹配任意 `notice` 类型事件,仅在需要同时捕获不同类型事件时使用。优先使用 notice type 的 Matcher。
## `REQUEST`
* **说明**: 匹配任意 `request` 类型事件,仅在需要同时捕获不同类型事件时使用。优先使用 request type 的 Matcher。
## `METAEVENT`
* **说明**: 匹配任意 `meta_event` 类型事件,仅在需要同时捕获不同类型事件时使用。优先使用 meta_event type 的 Matcher。
## `USER(*user, perm=<nonebot.permission.Permission object>)`
* **说明**
在白名单内且满足 perm
* **参数**
* `*user: int`: 白名单
* `perm: Permission`: 需要同时满足的权限
## `PRIVATE`
* **说明**: 匹配任意私聊消息类型事件
## `PRIVATE_FRIEND`
* **说明**: 匹配任意好友私聊消息类型事件
## `PRIVATE_GROUP`
* **说明**: 匹配任意群临时私聊消息类型事件
## `PRIVATE_OTHER`
* **说明**: 匹配任意其他私聊消息类型事件
## `GROUP`
* **说明**: 匹配任意群聊消息类型事件
## `GROUP_MEMBER`
* **说明**: 匹配任意群员群聊消息类型事件
:::warning 警告
该权限通过 event.sender 进行判断且不包含管理员以及群主!
:::
## `GROUP_ADMIN`
* **说明**: 匹配任意群管理员群聊消息类型事件
## `GROUP_OWNER`
* **说明**: 匹配任意群主群聊消息类型事件
## `SUPERUSER`
* **说明**: 匹配任意超级用户消息类型事件
## `EVERYBODY`
* **说明**: 匹配任意消息类型事件

122
docs/api/rule.md Normal file
View File

@ -0,0 +1,122 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.rule 模块
## 规则
每个 `Matcher` 拥有一个 `Rule` ,其中是 **异步** `RuleChecker` 的集合,只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
:::tip 提示
`RuleChecker` 既可以是 async function 也可以是 sync function
:::
## _class_ `Rule`
基类:`object`
* **说明**
`Matcher` 规则类,当事件传递时,在 `Matcher` 运行前进行检查。
* **示例**
```python
Rule(async_function) & sync_function
# 等价于
from nonebot.utils import run_sync
Rule(async_function, run_sync(sync_function))
```
### `__init__(*checkers)`
* **参数**
* `*checkers: Callable[[Bot, Event, dict], Awaitable[bool]]`: **异步** RuleChecker
### `checkers`
* **说明**
存储 `RuleChecker`
* **类型**
* `Set[Callable[[Bot, Event, dict], Awaitable[bool]]]`
### _async_ `__call__(bot, event, state)`
* **说明**
检查是否符合所有规则
* **参数**
* `bot: Bot`: Bot 对象
* `event: Event`: Event 对象
* `state: dict`: 当前 State
* **返回**
* `bool`
## `startswith(msg)`
* **说明**
匹配消息开头
* **参数**
* `msg: str`: 消息开头字符串
## `endswith(msg)`
* **说明**
匹配消息结尾
* **参数**
* `msg: str`: 消息结尾字符串

41
docs/api/sched.md Normal file
View File

@ -0,0 +1,41 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.sched 模块
## 计划任务
计划任务使用第三方库 [APScheduler](https://github.com/agronholm/apscheduler) ,使用文档请参考 [APScheduler使用文档](https://apscheduler.readthedocs.io/en/latest/) 。
## `scheduler`
* **类型**
`Optional[apscheduler.schedulers.asyncio.AsyncIOScheduler]`
* **说明**
当可选依赖 `APScheduler` 未安装时,`scheduler` 为 None
使用 `pip install nonebot[scheduler]` 安装可选依赖
* **常用示例**
```python
from nonebot import scheduler
@scheduler.scheduled_job("cron", hour="*/2", id="xxx", args=[1], kwargs={arg2: 2})
async def run_every_2_hour(arg1, arg2):
pass
scheduler.add_job(run_every_day_from_program_start, "interval", days=1, id="xxx")
```

View File

@ -19,7 +19,7 @@ sidebarDepth: 0
* **类型** * **类型**
BaseDriver `BaseDriver`
@ -35,7 +35,7 @@ sidebarDepth: 0
* **类型** * **类型**
BaseWebSocket `BaseWebSocket`
@ -51,7 +51,7 @@ sidebarDepth: 0
* **类型** * **类型**
BaseBot `BaseBot`
@ -67,7 +67,7 @@ sidebarDepth: 0
* **类型** * **类型**
BaseEvent `BaseEvent`
@ -83,7 +83,7 @@ sidebarDepth: 0
* **类型** * **类型**
BaseMessage `BaseMessage`
@ -99,7 +99,7 @@ sidebarDepth: 0
* **类型** * **类型**
BaseMessageSegment `BaseMessageSegment`
@ -115,7 +115,7 @@ sidebarDepth: 0
* **类型** * **类型**
Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]] `Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]`
@ -131,7 +131,7 @@ sidebarDepth: 0
* **类型** * **类型**
Matcher `Matcher`
@ -147,7 +147,7 @@ sidebarDepth: 0
* **类型** * **类型**
Rule `Rule`
@ -163,7 +163,7 @@ sidebarDepth: 0
* **类型** * **类型**
Callable[[Bot, Event, dict], Awaitable[bool]] `Callable[[Bot, Event, dict], Union[bool, Awaitable[bool]]]`
@ -179,7 +179,7 @@ sidebarDepth: 0
* **类型** * **类型**
Permission `Permission`
@ -195,7 +195,7 @@ sidebarDepth: 0
* **类型** * **类型**
Callable[[Bot, Event], Awaitable[bool]] `Callable[[Bot, Event], Union[bool, Awaitable[bool]]]`
@ -211,7 +211,7 @@ sidebarDepth: 0
* **类型** * **类型**
Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]] `Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]`
@ -227,7 +227,7 @@ sidebarDepth: 0
* **类型** * **类型**
Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]] `Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]`

39
docs/api/utils.md Normal file
View File

@ -0,0 +1,39 @@
---
contentSidebar: true
sidebarDepth: 0
---
# NoneBot.utils 模块
## `run_sync(func)`
* **说明**
一个用于包装 sync function 为 async function 的装饰器
* **参数**
* `func: Callable[..., Any]`: 被装饰的同步函数
* **返回**
* `Callable[..., Awaitable[Any]]`
## _class_ `DataclassEncoder`
基类:`json.encoder.JSONEncoder`
* **说明**
在JSON序列化 `Message` (List[Dataclass]) 时使用的 `JSONEncoder`

38
docs/guide/README.md Normal file
View File

@ -0,0 +1,38 @@
# 概览
:::tip 提示
如果在阅读本文档时遇到难以理解的词汇,请随时查阅 [术语表](../glossary.md) 或使用 [Google 搜索](https://www.google.com/)。
:::
:::tip 提示
初次使用时可能会觉得这里的概览过于枯燥,可以先简单略读之后直接前往 [安装](./installation.md) 查看安装方法,并进行后续的基础使用教程。
:::
NoneBot2 是一个可扩展的 Python 异步机器人框架,它会对机器人收到的消息进行解析和处理,并以插件化的形式,分发给消息所对应的命令处理器和自然语言处理器,来完成具体的功能。
除了起到解析消息的作用NoneBot 还为插件提供了大量实用的预设操作和权限控制机制,尤其对于命令处理器,它更是提供了完善且易用的会话机制和内部调用机制,以分别适应命令的连续交互和插件内部功能复用等需求。
目前 NoneBot2 在 [FastAPI](https://fastapi.tiangolo.com/) 的基础上封装了与 [CQHTTP(OneBot) 协议](http://cqhttp.cc/)插件的网络交互。
得益于 Python 的 [asyncio](https://docs.python.org/3/library/asyncio.html) 机制NoneBot 处理消息的吞吐量有了很大的保障,再配合 WebSocket 通信方式也是最建议的通信方式NoneBot 的性能可以达到 HTTP 通信方式的两倍以上,相较于传统同步 I/O 的 HTTP 通信,更是有质的飞跃。
需要注意的是NoneBot 仅支持 Python 3.7+ 及 CQHTTP(OneBot) 插件 v11+。
## 它如何工作?
NoneBot 的运行离不开 酷 Q 和 CQHTTP 插件。酷 Q 扮演着「无头 QQ 客户端」的角色,它进行实际的消息、通知、请求的接收和发送,当 酷 Q 收到消息时,它将这个消息包装为一个事件(通知和请求同理),并通过它自己的插件机制将事件传送给 CQHTTP 插件,后者再根据其配置中的 `post_url``ws_reverse_url` 等项来将事件发送至 NoneBot。
在 NoneBot 收到事件前,它底层的 aiocqhttp 实际已经先看到了事件aiocqhttp 根据事件的类型信息,通知到 NoneBot 的相应函数。特别地,对于消息类型的事件,还将消息内容转换成了 `aiocqhttp.message.Message` 类型,以便处理。
NoneBot 的事件处理函数收到通知后对于不同类型的事件再做相应的预处理和解析然后调用对应的插件并向其提供适合此类事件的会话Session对象。NoneBot 插件的编写者要做的,就是利用 Session 对象中提供的数据,在插件的处理函数中实现所需的功能。
## 特色
- 提供直观的测试前端
- 提供使用简易的脚手架
- 基于异步 I/O
- 同时支持 HTTP 和反向 WebSocket 通信方式
- 支持多个机器人账号负载均衡
- 提供直观的交互式会话接口
- 提供可自定义的权限控制机制
- 多种方式渲染要发送的消息内容,使对话足够自然

View File

@ -0,0 +1,68 @@
# 基本配置
到目前为止我们还在使用 NoneBot 的默认行为,在开始编写自己的插件之前,我们先尝试在配置文件上动动手脚,让 NoneBot 表现出不同的行为。
在上一章节中,我们创建了默认的项目结构,其中 `.env`, `.env.*` 均为项目的配置文件,下面将介绍几种 NoneBot 配置方式。
:::danger 警告
请勿将敏感信息写入配置文件并提交至开源仓库!
:::
## .env 文件
NoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找变量 `ENVIRONMENT` (大小写不敏感),默认值为 `prod`
这将引导 NoneBot 从系统环境变量或者 `.env.{ENVIRONMENT}` 文件中进一步加载具体配置。
现在,我们在 `.env` 文件中写入当前环境信息
```bash
# .env
ENVIRONMENT=dev
```
## .env.\* 文件
详细配置文件,使用 [pydantic](https://pydantic-docs.helpmanual.io/) 加载配置。在 NoneBot 初始化时可以指定忽略 `.env` 中的环境信息转而加载某个配置文件: `nonebot.init(_env_file=".env.dev")`
:::warning 提示
由于 `pydantic` 使用 JSON 加载配置项,请确保配置项值为 JSON 能够解析的数据。如果 JSON 解析失败将作为字符串处理。
:::
示例及说明:
```bash
HOST=0.0.0.0 # 配置 NoneBot 监听的 IP/主机名
PORT=8080 # 配置 NoneBot 监听的端口
DEBUG=true # 开启 debug 模式 **请勿在生产环境开启**
SUPERUSERS=["123456789", "987654321"] # 配置 NoneBot 超级用户
NICKNAME=["awesome", "bot"] # 配置机器人的昵称
COMMAND_START=["/", ""] # 配置命令起始字符
COMMAND_SEP=["."] # 配置命令分割字符
# Custom Configs
CUSTOM_CONFIG1="config in env file"
CUSTOM_CONFIG2= # 留空则从系统环境变量读取,如不存在则为空字符串
```
详细的配置项参考 [Config Reference](../api/config.md) 。
## bot.py 文件
配置项也可以在 NoneBot 初始化时传入。此处可以传入任意合法 Python 变量。当然也可以在初始化完成后修改或新增。
示例:
```python
# bot.py
import nonebot
nonebot.init(custom_config3="config on init")
config = nonebot.get_driver().config
config.custom_config3 = "changed after init"
config.custom_config4 = "new config after init"
```
## 优先级
`bot.py init` > `env file` > `system env`

View File

@ -0,0 +1,55 @@
# 创建一个完整的项目
上一章中我们已经运行了一个最小的 NoneBot 实例,在这一章,我们将从零开始一个完整的项目。
## 目录结构
首先,我们可以使用 `nb-cli` 或者自行创建项目目录:
```bash
pip install nonebot2[cli]
# pip install nb-cli
nb create
```
这将创建默认的目录结构
<!-- prettier-ignore-start -->
:::vue
AweSome-Bot
├── `awesome_bot` _(**或是 src**)_
│ └── `plugins`
├── `.env` _(**可选的**)_
├── `.env.dev` _(**可选的**)_
├── `.env.prod` _(**可选的**)_
├── .gitignore
├── `bot.py`
├── docker-compose.yml
├── Dockerfile
├── `pyproject.toml`
└── README.md
:::
<!-- prettier-ignore-end -->
- `awesome_bot/plugins``src/plugins`: 用于存放编写的 bot 插件
- `.env`, `.env.dev`, `.env.prod`: 各环境配置文件
- `bot.py`: bot 入口文件
- `pyproject.toml`: 项目依赖管理文件,默认使用 [poetry](https://python-poetry.org/)
## 启动 Bot
如果你使用 `nb-cli`
```bash
nb run [--file=bot.py] [--app=app]
```
或者使用
```bash
python bot.py
```
:::tip 提示
如果在 bot 入口文件内定义了 asgi server `nb-cli` 将会为你启动**冷重载模式**
:::

View File

@ -0,0 +1,146 @@
# 开始使用
一切都安装成功后,你就已经做好了进行简单配置以运行一个最小的 NoneBot 实例的准备。
## 最小实例
使用你最熟悉的编辑器或 IDE创建一个名为 `bot.py` 的文件,内容如下:
```python{3,4,7}
import nonebot
nonebot.init()
nonebot.load_builtin_plugins()
if __name__ == "__main__":
nonebot.run()
```
这几行高亮代码将依次:
1. 使用默认配置初始化 NoneBot 包
2. 加载 NoneBot 内置的插件
3. 在地址 `127.0.0.1:8080` 运行 NoneBot
在命令行使用如下命令即可运行这个 NoneBot 实例:
```bash
python bot.py
```
运行后会产生如下日志:
```default
09-14 21:02:00 [INFO] nonebot | Succeeded to import "nonebot.plugins.base"
09-14 21:02:00 [INFO] nonebot | Running NoneBot...
09-14 21:02:00 [INFO] uvicorn | Started server process [1234]
09-14 21:02:00 [INFO] uvicorn | Waiting for application startup.
09-14 21:02:00 [INFO] nonebot | Scheduler Started
09-14 21:02:00 [INFO] uvicorn | Application startup complete.
09-14 21:02:00 [INFO] uvicorn | Uvicorn running on http://127.0.0.1:8080 (Press CTRL+C to quit)
```
## 配置 QQ 协议端
单纯运行 NoneBot 实例并不会产生任何效果,因为此刻 QQ 这边还不知道 NoneBot 的存在,也就无法把消息发送给它,因此现在需要使用一个无头 QQ 来把消息等事件上报给 NoneBot。
目前支持的协议有:
- [OneBot(CQHTTP)](https://github.com/howmanybots/onebot)
QQ 协议端举例:
- [Mirai](https://github.com/mamoe/mirai) + [cqhttp-mirai](https://github.com/yyuueexxiinngg/cqhttp-mirai)
- [cqhttp-mirai-embedded](https://github.com/yyuueexxiinngg/cqhttp-mirai/tree/embedded)
- [Mirai](https://github.com/mamoe/mirai) + [Mirai Native](https://github.com/iTXTech/mirai-native) + [CQHTTP](https://github.com/richardchien/coolq-http-api)
- [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) (基于 [MiraiGo](https://github.com/Mrs4s/MiraiGo))
- [OICQ-http-api](https://github.com/takayama-lily/onebot) (基于 [OICQ](https://github.com/takayama-lily/oicq))
这里以 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 为例
1. 下载 go-cqhttp 对应平台的 release 文件
2. 双击 exe 文件或者使用 `./go-cqhttp` 启动
3. 生成默认配置文件并修改默认配置
```json{2,3,30-31}
{
"uin": 你的QQ号,
"password": "你的密码",
"encrypt_password": false,
"password_encrypted": "",
"enable_db": true,
"access_token": "",
"relogin": {
"enabled": true,
"relogin_delay": 3,
"max_relogin_times": 0
},
"ignore_invalid_cqcode": false,
"force_fragmented": true,
"heartbeat_interval": 0,
"http_config": {
"enabled": false,
"host": "0.0.0.0",
"port": 5700,
"timeout": 0,
"post_urls": {}
},
"ws_config": {
"enabled": false,
"host": "0.0.0.0",
"port": 6700
},
"ws_reverse_servers": [
{
"enabled": true,
"reverse_url": "ws://127.0.0.1:8080/cqhttp/ws",
"reverse_api_url": "",
"reverse_event_url": "",
"reverse_reconnect_interval": 3000
}
],
"post_message_format": "string",
"debug": false,
"log_level": ""
}
```
其中 `ws://127.0.0.1:8080/cqhttp/ws` 中的 `127.0.0.1``8080` 应分别对应 nonebot 配置的 HOST 和 PORT
## 历史性的第一次对话
一旦新的配置文件正确生效之后NoneBot 所在的控制台(如果正在运行的话)应该会输出类似下面的内容(两条访问日志):
```default
09-14 21:31:16 [INFO] uvicorn | ('127.0.0.1', 12345) - "WebSocket /cqhttp/ws" [accepted]
09-14 21:31:16 [INFO] nonebot | WebSocket Connection from CQHTTP Bot 你的QQ号 Accepted!
```
这表示 QQ 协议端已经成功地使用 CQHTTP 协议连接上了 NoneBot。
:::warning 注意
如果到这一步你没有看到上面这样的成功日志CQHTTP 的日志中在不断地重连或无反应,请注意检查配置中的 IP 和端口是否确实可以访问。比较常见的出错点包括:
- NoneBot 监听 `0.0.0.0`,然后在 CQHTTP 配置中填了 `ws://0.0.0.0:8080/cqhttp/ws`
- 在 Docker 容器内运行 CQHTTP并通过 `127.0.0.1` 访问宿主机上的 NoneBot
- 想从公网访问,但没有修改云服务商的安全组策略或系统防火墙
- NoneBot 所监听的端口存在冲突,已被其它程序占用
- 弄混了 NoneBot 的 `host``port` 参数与 CQHTTP 配置中的 `host``port` 参数
- 使用了 `ws_reverse_api_url``ws_reverse_event_url` 而非 universal client
- `ws://` 错填为 `http://`
- CQHTTP 或 NoneBot 启动时遭到外星武器干扰
请尝试重启 CQHTTP、重启 NoneBot、更换端口、修改防火墙、重启系统、仔细阅读前面的文档及提示、更新 CQHTTP 和 NoneBot 到最新版本等方式来解决。
:::
现在,尝试向你的 QQ 机器人账号发送如下内容:
```default
/say 你好,世界
```
到这里如果一切 OK你应该会收到机器人给你回复了 `你好,世界`。这一历史性的对话标志着你已经成功地运行了一个 NoneBot 的最小实例,开始了编写更强大的 QQ 机器人的创意之旅!
<ClientOnly>
<Messenger :messages="[{ position: 'right', msg: '/say 你好,世界' }, { position: 'left', msg: '你好,世界' }]"/>
</ClientOnly>

View File

@ -0,0 +1,73 @@
# 安装
## NoneBot
:::warning 注意
请确保你的 Python 版本 >= 3.7。
:::
```bash
pip install nonebot2
```
如果你需要使用最新的(可能尚未发布的)特性,可以克隆 Git 仓库后手动安装:
```bash
git clone https://github.com/nonebot/nonebot2.git
cd nonebot2
poetry install --no-dev # 推荐
pip install . # 不推荐
```
## 额外依赖
### APScheduler
A task scheduling library for Python.
可用于计划任务,后台执行任务等
```bash
pip install nonebot2[scheduler]
poetry add nonebot2[scheduler]
```
[View On GitHub](https://github.com/agronholm/apscheduler)
### NoneBot-Test
A test frontend for nonebot2.
通过前端展示 nonebot 已加载的插件以及运行状态,同时可以用于模拟发送事件测试机器人
```bash
pip install nonebot2[test]
poetry add nonebot2[test]
```
[View On GitHub](https://github.com/nonebot/nonebot-test)
### CLI
CLI for nonebot2.
一个多功能脚手架
```bash
pip install nonebot2[cli]
poetry add nonebot2[cli]
```
[View On GitHub](https://github.com/yanyongyu/nb-cli)
### 我全都要
```bash
pip install nonebot2[full]
poetry add nonebot2[full]
```
```bash
pip install nonebot2[cli,scheduler]
poetry add nonebot2[cli,scheduler]
```

View File

@ -0,0 +1,290 @@
# 编写插件
本章将以一个天气查询插件为例,教学如何编写自己的命令。
## 加载插件
在 [创建一个完整的项目](creating-a-project) 一章节中,我们已经创建了插件目录 `awesome_bot/plugins`,现在我们在机器人入口文件中加载它。当然,你也可以单独加载一个插件。
:::tip 提示
加载插件目录时,目录下以 `_` 下划线开头的插件将不会被加载!
:::
`bot.py` 文件中添加以下行:
```python{5,7}
import nonebot
nonebot.init()
# 加载单独的一个插件参数为合法的python包名
nonebot.load_plugin("nonebot.plugins.base")
# 加载插件目录,该目录下为各插件,以下划线开头的插件将不会被加载
nonebot.load_plugins("awesome_bot/plugins")
app = nonebot.get_asgi()
if __name__ == "__main__":
nonebot.run()
```
尝试运行 `nb run` 或者 `python bot.py`,可以看到日志输出了类似如下内容:
```plain
09-19 21:51:59 [INFO] nonebot | Succeeded to import "nonebot.plugins.base"
09-19 21:51:59 [INFO] nonebot | Succeeded to import "plugin_in_folder"
```
## 创建插件
现在我们已经有了一个空的插件目录,我们可以开始创建插件了!插件有两种形式
### 单文件形式
在插件目录下创建名为 `weather.py` 的 Python 文件,暂时留空,此时目录结构如下:
<!-- prettier-ignore-start -->
:::vue
AweSome-Bot
├── awesome_bot
│ └── plugins
│ └── `weather.py`
├── .env
├── .env.dev
├── .env.prod
├── .gitignore
├── bot.py
├── docker-compose.yml
├── Dockerfile
├── pyproject.toml
└── README.md
:::
<!-- prettier-ignore-end -->
这个时候它已经可以被称为一个插件了,尽管它还什么都没做。
### 包形式
在插件目录下创建文件夹 `weather`,并在该文件夹下创建文件 `__init__.py`,此时目录结构如下:
<!-- prettier-ignore-start -->
:::vue
AweSome-Bot
├── awesome_bot
│ └── plugins
│ └── `weather`
│ └── `__init__.py`
├── .env
├── .env.dev
├── .env.prod
├── .gitignore
├── bot.py
├── docker-compose.yml
├── Dockerfile
├── pyproject.toml
└── README.md
:::
<!-- prettier-ignore-end -->
这个时候 `weather` 就是一个合法的 Python 包了,同时也是合法的 NoneBot 插件,插件内容可以在 `__init__.py` 中编写。
## 编写真正的内容
好了,现在插件已经可以正确加载,我们可以开始编写命令的实际代码了。在 `weather.py` 中添加如下代码:
```python
from nonebot import on_command
from nonebot.rule import to_me
from nonebot.adapters.cqhttp import Bot, Event
weather = on_command("天气", rule=to_me(), priority=5)
@weather.handle()
async def handle_first_receive(bot: Bot, event: Event, state: dict):
args = str(event.message).strip() # 首次发送命令时跟随的参数,例:/天气 上海则args为上海
if args:
state["city"] = args # 如果用户发送了参数则直接赋值
@weather.got("city", prompt="你想查询哪个城市的天气呢?")
async def handle_city(bot: Bot, event: Event, state: dict):
city = state["city"]
if city not in ["上海", "北京"]:
await weather.reject("你想查询的城市暂不支持,请重新输入!")
city_weather = await get_weather(city)
await weather.finish(city_weather)
async def get_weather(city: str):
return f"{city}的天气是..."
```
为了简单起见,我们在这里的例子中没有接入真实的天气数据,但要接入也非常简单,你可以使用中国天气网、和风天气等网站提供的 API。
下面我们来说明这段代码是如何工作的。
:::tip 提示
从这里开始,你需要对 Python 的 asyncio 编程有所了解,因为 NoneBot 是完全基于 asyncio 的,具体可以参考 [廖雪峰的 Python 教程](https://www.liaoxuefeng.com/wiki/1016959663602400/1017959540289152)
:::
### 注册一个 [事件响应器](../api/matcher.md)
```python{4}
from nonebot import on_command
from nonebot.rule import to_me
from nonebot.permission import Permission
weather = on_command("天气", rule=to_me(), permission=Permission(), priority=5)
```
在上方代码中,我们注册了一个事件响应器 `Matcher`,它由几个部分组成:
1. `on_command` 注册一个消息类型的命令处理器
2. `"天气"` 指定 command 参数 - 命令名
3. `rule` 补充事件响应器的匹配规则
4. `priority` 事件响应器优先级
5. `block` 是否阻止事件传递
其他详细配置可以参考 API 文档,下面我们详细说明各个部分:
#### 事件响应器类型 type
事件响应器类型其实就是对应 `Event.type` NoneBot 提供了一个基础类型事件响应器 `on()` 以及一些内置的事件响应器。
- `on("事件类型")`: 基础事件响应器,第一个参数为事件类型,空字符串表示不限
- `on_metaevent()` ~ `on("meta_event")`: 元事件响应器
- `on_message()` ~ `on("message")`: 消息事件响应器
- `on_request()` ~ `on("request")`: 请求事件响应器
- `on_notice()` ~ `on("notice")`: 通知事件响应器
- `on_startswith(str)` ~ `on("message", startswith(str))`: 消息开头匹配处理器
- `on_endswith(str)` ~ `on("message", endswith(str))`: 消息结尾匹配处理器
- `on_command(str|tuple)` ~ `on("message", command(str|tuple))`: 命令处理器
- `on_regax(pattern_str)` ~ `on("message", regax(pattern_str))`: 正则匹配处理器
#### 匹配规则 rule
事件响应器的匹配规则即 `Rule`,由非负个 `RuleChecker` 组成,当所有 `RuleChecker` 返回 `True` 时匹配成功。这些 `RuleChecker` 的形式如下:
```python
async def check(bot: Bot, event: Event, state: dict) -> bool:
return True
def check(bot: Bot, event: Event, state: dict) -> bool:
return True
```
`Rule``RuleChecker` 之间可以使用 `与 &` 互相组合:
```python
from nonebot.rule import Rule
Rule(async_checker1) & sync_checker & async_checker2
```
:::danger 警告
`Rule(*checkers)` 只接受 async function或使用 `nonebot.utils.run_sync` 自行包裹 sync function。在使用 `与 &`NoneBot 会自动包裹 sync function
:::
#### 优先级 priority
事件响应器的优先级代表事件响应器的执行顺序,同一优先级的事件响应器会 **同时执行!**
:::tip 提示
使用 `nonebot-test` 可以看到当前所有事件响应器的执行流程,有助理解事件响应流程!
```bash
pip install nonebot2[test]
```
:::
#### 阻断 block
当有任意事件响应器发出了阻止事件传递信号时,该事件将不再会传递给下一优先级,直接结束处理。
NoneBot 内置的事件响应器中,所有 `message` 类的事件响应器默认会阻断事件传递,其他则不会。
### 编写事件处理函数 [Handler](../api/typing.md#handler)
```python{1,2,8,9}
@weather.handle()
async def handle_first_receive(bot: Bot, event: Event, state: dict):
args = str(event.message).strip() # 首次发送命令时跟随的参数,例:/天气 上海则args为上海
if args:
state["city"] = args # 如果用户发送了参数则直接赋值
@weather.got("city", prompt="你想查询哪个城市的天气呢?")
async def handle_city(bot: Bot, event: Event, state: dict):
city = state["city"]
if city not in ["上海", "北京"]:
await weather.reject("你想查询的城市暂不支持,请重新输入!")
city_weather = await get_weather(city)
await weather.finish(city_weather)
```
在上面的代码中,我们给 `weather` 事件响应器添加了两个事件处理函数:`handle_first_receive`, `handle_city`
其中有几个要点,我们一一解释:
#### 添加一个事件处理函数
在事件响应器响应事件时,事件处理函数会依次顺序执行,也就是说,与添加顺序一致。
我们可以使用 `@matcher.handle()` 装饰器来简单地为该事件响应器添加一个处理函数。
同时NoneBot 内置了几种添加事件处理函数方式以方便处理:
- `@matcher.receive()`: 指示 NoneBot 接收一条新的用户消息以继续执行后续处理函数。
- `@matcher.got(key, [prompt="请输入key"], [args_parser=function])`: 指示 NoneBot 当 `state` 中不存在 `key` 时向用户发送 `prompt` 等待用户回复并赋值给 `state[key]`
这些装饰器可以套娃使用!例如:
```python
@matcher.got("key1")
@matcher.got("key2")
async def handle(bot: Bot, event: Event, state: dict):
pass
```
#### 事件处理函数参数
事件处理函数类型为 `Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]`
参数分别为:
1. [nonebot.typing.Bot](../api/typing.md#bot): 即事件上报连接对应的 Bot 对象,为 BaseBot 的子类。特别注意,此处的类型注释可以替换为指定的 Bot 类型,例如:`nonebot.adapters.cqhttp.Bot`,只有在上报事件的 Bot 类型与类型注释相符时才会执行该处理函数!可用于多平台进行不同的处理。
2. [nonebot.typing.Event](../api/typing.md#event): 即上报事件对象,可以获取到上报的所有信息。
3. `state`: 状态字典,可以存储任意的信息
#### 处理事件
在事件处理函数中,我们只需要对 `event` 做出相应的处理,存入状态字典 `state` 中,或者向用户发送消息、调用某个机器人 API 等等。
在 NoneBot 中,提供了几种特殊的处理函数:
##### `@matcher.args_parser`
这是一个装饰器,装饰一个函数来使它成为参数的默认解析函数,当使用 `matcher.got(xxx, [args_parser])` 获取到一条消息时,会运行 `matcher.got``args_parser` ,如果不存在则运行 `@matcher.args_parser`
##### `matcher.pause`
这个函数用于结束当前事件处理函数,强制接收一条新的消息再运行**下一个消息处理函数**。
##### `matcher.reject`
这个函数用于结束当前事件处理函数,强制接收一条新的消息再**再次运行当前消息处理函数**。
##### `matcher.finish`
这个函数用于直接结束当前事件处理。
以上三个函数都拥有一个参数 `prompt`,用于向用户发送一条消息。
## 结语
至此,相信你已经能够写出一个基础的插件了,更多的用法将会在 进阶 部分进行介绍,这里给出几个小提示:
- 请千万注意事件处理器的优先级设定
- 在匹配规则中请勿使用耗时极长的函数
- 同一个用户可以跨群(私聊)继续他的事件处理(除非做出权限限制,将在后续介绍)

View File

@ -4,5 +4,10 @@ NoneBot Api Reference
:模块索引: :模块索引:
- `nonebot <nonebot.html>`_ - `nonebot <nonebot.html>`_
- `nonebot.typing <typing.html>`_ - `nonebot.typing <typing.html>`_
- `nonebot.log <log.html>`_
- `nonebot.config <config.html>`_ - `nonebot.config <config.html>`_
- `nonebot.sched <sched.html>`_
- `nonebot.log <log.html>`_
- `nonebot.rule <rule.html>`_
- `nonebot.permission <permission.html>`_
- `nonebot.utils <utils.html>`_
- `nonebot.exception <exception.html>`_

11
docs_build/permission.rst Normal file
View File

@ -0,0 +1,11 @@
---
contentSidebar: true
sidebarDepth: 0
---
NoneBot.permission 模块
====================
.. automodule:: nonebot.permission
:members:
:show-inheritance:

12
docs_build/rule.rst Normal file
View File

@ -0,0 +1,12 @@
---
contentSidebar: true
sidebarDepth: 0
---
NoneBot.rule 模块
====================
.. automodule:: nonebot.rule
:members:
:special-members:
:show-inheritance:

11
docs_build/sched.rst Normal file
View File

@ -0,0 +1,11 @@
---
contentSidebar: true
sidebarDepth: 0
---
NoneBot.sched 模块
===================
.. automodule:: nonebot.sched
:members:
:show-inheritance:

12
docs_build/utils.rst Normal file
View File

@ -0,0 +1,12 @@
---
contentSidebar: true
sidebarDepth: 0
---
NoneBot.utils 模块
==================
.. autodecorator:: nonebot.utils.run_sync
.. autoclass:: nonebot.utils.DataclassEncoder
:show-inheritance:

View File

@ -108,6 +108,7 @@ def get_bots() -> Union[NoReturn, Dict[str, Bot]]:
return driver.bots return driver.bots
from nonebot.sched import scheduler
from nonebot.config import Env, Config from nonebot.config import Env, Config
from nonebot.log import logger, default_filter from nonebot.log import logger, default_filter
from nonebot.adapters.cqhttp import Bot as CQBot from nonebot.adapters.cqhttp import Bot as CQBot
@ -135,7 +136,7 @@ def init(*, _env_file: Optional[str] = None, **kwargs):
:返回: :返回:
- `None` - ``None``
:用法: :用法:
@ -146,10 +147,10 @@ def init(*, _env_file: Optional[str] = None, **kwargs):
""" """
global _driver global _driver
if not _driver: if not _driver:
logger.debug("NoneBot is initializing...") logger.info("NoneBot is initializing...")
env = Env() env = Env()
logger.opt( logger.opt(
colors=True).debug(f"Current <y><b>Env: {env.environment}</b></y>") colors=True).info(f"Current <y><b>Env: {env.environment}</b></y>")
config = Config(**kwargs, config = Config(**kwargs,
_env_file=_env_file or f".env.{env.environment}") _env_file=_env_file or f".env.{env.environment}")
@ -169,6 +170,9 @@ def init(*, _env_file: Optional[str] = None, **kwargs):
logger.debug("Loading nonebot test frontend...") logger.debug("Loading nonebot test frontend...")
nonebot_test.init() nonebot_test.init()
if scheduler:
_driver.on_startup(_start_scheduler)
def run(host: Optional[str] = None, def run(host: Optional[str] = None,
port: Optional[int] = None, port: Optional[int] = None,
@ -188,7 +192,7 @@ def run(host: Optional[str] = None,
:返回: :返回:
- `None` - ``None``
:用法: :用法:
@ -201,6 +205,13 @@ def run(host: Optional[str] = None,
get_driver().run(host, port, *args, **kwargs) get_driver().run(host, port, *args, **kwargs)
async def _start_scheduler():
if scheduler and not scheduler.running:
scheduler.configure(_driver.config.apscheduler_config)
scheduler.start()
logger.opt(colors=True).info("<y>Scheduler Started</y>")
from nonebot.plugin import on_message, on_notice, on_request, on_metaevent from nonebot.plugin import on_message, on_notice, on_request, on_metaevent
from nonebot.plugin import on_startswith, on_endswith, on_command, on_regex from nonebot.plugin import on_startswith, on_endswith, on_command, on_regex
from nonebot.plugin import load_plugin, load_plugins, load_builtin_plugins, get_loaded_plugins from nonebot.plugin import load_plugin, load_plugins, load_builtin_plugins, get_loaded_plugins

View File

@ -61,13 +61,16 @@ async def _check_reply(bot: "Bot", event: "Event"):
if event.type != "message": if event.type != "message":
return return
first_msg_seg = event.message[0] try:
if first_msg_seg.type == "reply": index = list(map(lambda x: x.type == "reply",
msg_id = first_msg_seg.data["id"] event.message)).index(True)
event.reply = await bot.get_msg(message_id=msg_id) except ValueError:
return
msg_seg = event.message[index]
event.reply = await bot.get_msg(message_id=msg_seg.data["id"])
if event.reply["sender"]["user_id"] == event.self_id: if event.reply["sender"]["user_id"] == event.self_id:
event.to_me = True event.to_me = True
del event.message[0] del event.message[index]
def _check_at_me(bot: "Bot", event: "Event"): def _check_at_me(bot: "Bot", event: "Event"):
@ -84,6 +87,18 @@ def _check_at_me(bot: "Bot", event: "Event"):
if first_msg_seg == at_me_seg: if first_msg_seg == at_me_seg:
event.to_me = True event.to_me = True
del event.message[0] del event.message[0]
if event.message[0].type == "text":
event.message[0].data["text"] = event.message[0].data[
"text"].lstrip()
if not event.message[0].data["text"]:
del event.message[0]
if event.message[0] == at_me_seg:
del event.message[0]
if event.message[0].type == "text":
event.message[0].data["text"] = event.message[0].data[
"text"].lstrip()
if not event.message[0].data["text"]:
del event.message[0]
if not event.to_me: if not event.to_me:
# check the last segment # check the last segment
@ -199,6 +214,7 @@ class Bot(BaseBot):
ResultStore.add_result(message) ResultStore.add_result(message)
return return
try:
event = Event(message) event = Event(message)
# Check whether user is calling me # Check whether user is calling me
@ -207,6 +223,10 @@ class Bot(BaseBot):
_check_nickname(self, event) _check_nickname(self, event)
await handle_event(self, event) await handle_event(self, event)
except Exception as e:
logger.opt(colors=True, exception=e).error(
f"<r><bg #f8bbd0>Failed to handle event. Raw: {message}</bg #f8bbd0></r>"
)
@overrides(BaseBot) @overrides(BaseBot)
async def call_api(self, api: str, **data) -> Union[Any, NoReturn]: async def call_api(self, api: str, **data) -> Union[Any, NoReturn]:

View File

@ -79,6 +79,9 @@ class BaseConfig(BaseSettings):
if env_file_vars: if env_file_vars:
for env_name, env_val in env_file_vars.items(): for env_name, env_val in env_file_vars.items():
if (env_val is None or
len(env_val) == 0) and env_name in env_vars:
env_val = env_vars[env_name]
try: try:
env_val = self.__config__.json_loads(env_val) env_val = self.__config__.json_loads(env_val)
except ValueError as e: except ValueError as e:
@ -131,7 +134,7 @@ class Config(BaseConfig):
- 类型: ``IPvAnyAddress`` - 类型: ``IPvAnyAddress``
- 默认值: ``127.0.0.1`` - 默认值: ``127.0.0.1``
- 说明: - 说明:
NoneBot HTTP WebSocket 服务端监听的 IP主机名 NoneBot HTTP WebSocket 服务端监听的 IP/主机名
""" """
port: int = 8080 port: int = 8080
""" """
@ -140,19 +143,6 @@ class Config(BaseConfig):
- 说明: - 说明:
NoneBot HTTP WebSocket 服务端监听的端口 NoneBot HTTP WebSocket 服务端监听的端口
""" """
secret: Optional[str] = None
"""
- 类型: ``Optional[str]``
- 默认值: ``None``
- 说明:
上报连接 NoneBot 所需的密钥
- 示例:
.. code-block:: http
POST /cqhttp/ HTTP/1.1
Authorization: Bearer kSLuTF2GC2Q4q4ugm3
"""
debug: bool = False debug: bool = False
""" """
- 类型: ``bool`` - 类型: ``bool``
@ -170,7 +160,7 @@ class Config(BaseConfig):
以机器人 ID 为键上报地址为值的字典环境变量或文件中应使用 json 序列化 以机器人 ID 为键上报地址为值的字典环境变量或文件中应使用 json 序列化
- 示例: - 示例:
.. code-block:: plain .. code-block:: default
API_ROOT={"123456": "http://127.0.0.1:5700"} API_ROOT={"123456": "http://127.0.0.1:5700"}
""" """
@ -186,7 +176,26 @@ class Config(BaseConfig):
- 类型: ``Optional[str]`` - 类型: ``Optional[str]``
- 默认值: ``None`` - 默认值: ``None``
- 说明: - 说明:
API 请求所需密钥会在调用 API 时在请求头中携带 API 请求以及上报所需密钥在请求头中携带
- 示例:
.. code-block:: http
POST /cqhttp/ HTTP/1.1
Authorization: Bearer kSLuTF2GC2Q4q4ugm3
"""
secret: Optional[str] = None
"""
- 类型: ``Optional[str]``
- 默认值: ``None``
- 说明:
HTTP POST 形式上报所需签名在请求头中携带
- 示例:
.. code-block:: http
POST /cqhttp/ HTTP/1.1
X-Signature: sha1=f9ddd4863ace61e64f462d41ca311e3d2c1176e2
""" """
# bot runtime configs # bot runtime configs
@ -198,7 +207,7 @@ class Config(BaseConfig):
机器人超级用户 机器人超级用户
- 示例: - 示例:
.. code-block:: plain .. code-block:: default
SUPER_USERS=[12345789] SUPER_USERS=[12345789]
""" """
@ -231,12 +240,22 @@ class Config(BaseConfig):
等待用户回复的超时时间 等待用户回复的超时时间
- 示例: - 示例:
.. code-block:: plain .. code-block:: default
SESSION_EXPIRE_TIMEOUT=120 # 单位: 秒 SESSION_EXPIRE_TIMEOUT=120 # 单位: 秒
SESSION_EXPIRE_TIMEOUT=[DD ][HH:MM]SS[.ffffff] SESSION_EXPIRE_TIMEOUT=[DD ][HH:MM]SS[.ffffff]
SESSION_EXPIRE_TIMEOUT=P[DD]DT[HH]H[MM]M[SS]S # ISO 8601 SESSION_EXPIRE_TIMEOUT=P[DD]DT[HH]H[MM]M[SS]S # ISO 8601
""" """
apscheduler_config: dict = {"apscheduler.timezone": "Asia/Shanghai"}
"""
- 类型: ``dict``
- 默认值: ``{"apscheduler.timezone": "Asia/Shanghai"}``
- 说明:
APScheduler 的配置对象 `Configuring the Scheduler`_
.. _Configuring the Scheduler:
https://apscheduler.readthedocs.io/en/latest/userguide.html#configuring-the-scheduler
"""
# custom configs # custom configs
# custom configs can be assigned during nonebot.init # custom configs can be assigned during nonebot.init

View File

@ -114,7 +114,8 @@ class Driver(BaseDriver):
adapter: str, adapter: str,
data: dict = Body(...), data: dict = Body(...),
x_self_id: Optional[str] = Header(None), x_self_id: Optional[str] = Header(None),
x_signature: Optional[str] = Header(None)): x_signature: Optional[str] = Header(None),
auth: Optional[str] = Depends(get_auth_bearer)):
# 检查self_id # 检查self_id
if not x_self_id: if not x_self_id:
logger.warning("Missing X-Self-ID Header") logger.warning("Missing X-Self-ID Header")
@ -135,6 +136,14 @@ class Driver(BaseDriver):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail="Signature is invalid") detail="Signature is invalid")
access_token = self.config.access_token
if access_token and access_token != auth:
logger.warning("Authorization Header is invalid"
if auth else "Missing Authorization Header")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail="Authorization Header is invalid"
if auth else "Missing Authorization Header")
if not isinstance(data, dict): if not isinstance(data, dict):
logger.warning("Data received is invalid") logger.warning("Data received is invalid")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
@ -161,22 +170,25 @@ class Driver(BaseDriver):
adapter: str, adapter: str,
websocket: FastAPIWebSocket, websocket: FastAPIWebSocket,
x_self_id: str = Header(None), x_self_id: str = Header(None),
access_token: Optional[str] = Depends(get_auth_bearer)): auth: Optional[str] = Depends(get_auth_bearer)):
ws = WebSocket(websocket) ws = WebSocket(websocket)
secret = self.config.secret access_token = self.config.access_token
if secret is not None and secret != access_token: if access_token and access_token != auth:
logger.warning("Authorization Header is invalid" logger.warning("Authorization Header is invalid"
if access_token else "Missing Authorization Header") if auth else "Missing Authorization Header")
await ws.close(code=status.WS_1008_POLICY_VIOLATION) await ws.close(code=status.WS_1008_POLICY_VIOLATION)
return
if not x_self_id: if not x_self_id:
logger.warning(f"Missing X-Self-ID Header") logger.warning(f"Missing X-Self-ID Header")
await ws.close(code=status.WS_1008_POLICY_VIOLATION) await ws.close(code=status.WS_1008_POLICY_VIOLATION)
return
if x_self_id in self._clients: if x_self_id in self._clients:
logger.warning(f"Connection Conflict: self_id {x_self_id}") logger.warning(f"Connection Conflict: self_id {x_self_id}")
await ws.close(code=status.WS_1008_POLICY_VIOLATION) await ws.close(code=status.WS_1008_POLICY_VIOLATION)
return
# Create Bot Object # Create Bot Object
if adapter in self._adapters: if adapter in self._adapters:

View File

@ -6,8 +6,8 @@ import typing
import inspect import inspect
from functools import wraps from functools import wraps
from datetime import datetime from datetime import datetime
from contextvars import ContextVar
from collections import defaultdict from collections import defaultdict
from contextvars import Context, ContextVar, copy_context
from nonebot.rule import Rule from nonebot.rule import Rule
from nonebot.permission import Permission, USER from nonebot.permission import Permission, USER
@ -101,8 +101,7 @@ class Matcher(metaclass=MatcherMeta):
@classmethod @classmethod
async def check_perm(cls, bot: Bot, event: Event) -> bool: async def check_perm(cls, bot: Bot, event: Event) -> bool:
return (event.type == (cls.type or event.type) and return await cls.permission(bot, event)
await cls.permission(bot, event))
@classmethod @classmethod
async def check_rule(cls, bot: Bot, event: Event, state: dict) -> bool: async def check_rule(cls, bot: Bot, event: Event, state: dict) -> bool:
@ -114,7 +113,8 @@ class Matcher(metaclass=MatcherMeta):
Returns: Returns:
bool: 条件成立与否 bool: 条件成立与否
""" """
return await cls.rule(bot, event, state) return (event.type == (cls.type or event.type) and
await cls.rule(bot, event, state))
@classmethod @classmethod
def args_parser(cls, func: ArgsParser) -> ArgsParser: def args_parser(cls, func: ArgsParser) -> ArgsParser:
@ -166,8 +166,8 @@ class Matcher(metaclass=MatcherMeta):
raise PausedException raise PausedException
async def _key_parser(bot: Bot, event: Event, state: dict): async def _key_parser(bot: Bot, event: Event, state: dict):
if key in state: # if key in state:
return # return
parser = args_parser or cls._default_parser parser = args_parser or cls._default_parser
if parser: if parser:
await parser(bot, event, state) await parser(bot, event, state)
@ -252,6 +252,7 @@ class Matcher(metaclass=MatcherMeta):
temp=True, temp=True,
priority=0, priority=0,
block=True, block=True,
module=self.module,
default_state=self.state, default_state=self.state,
expire_time=datetime.now() + bot.config.session_expire_timeout) expire_time=datetime.now() + bot.config.session_expire_timeout)
except PausedException: except PausedException:
@ -263,6 +264,7 @@ class Matcher(metaclass=MatcherMeta):
temp=True, temp=True,
priority=0, priority=0,
block=True, block=True,
module=self.module,
default_state=self.state, default_state=self.state,
expire_time=datetime.now() + bot.config.session_expire_timeout) expire_time=datetime.now() + bot.config.session_expire_timeout)
except FinishedException: except FinishedException:

View File

@ -1,19 +1,49 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
权限
====
每个 ``Matcher`` 拥有一个 ``Permission`` 其中是 **异步** ``PermissionChecker`` 的集合只要有一个 ``PermissionChecker`` 检查结果为 ``True`` 时就会继续运行
\:\:\:tip 提示
``PermissionChecker`` 既可以是 async function 也可以是 sync function
\:\:\:
"""
import asyncio import asyncio
from nonebot.utils import run_sync from nonebot.utils import run_sync
from nonebot.typing import Bot, Event, Union, NoReturn, PermissionChecker from nonebot.typing import Bot, Event, Union, NoReturn, Callable, Awaitable, PermissionChecker
class Permission: class Permission:
__slots__ = ("checkers",) __slots__ = ("checkers",)
def __init__(self, *checkers: PermissionChecker) -> None: def __init__(self, *checkers: Callable[[Bot, Event],
self.checkers = list(checkers) Awaitable[bool]]) -> None:
"""
:参数:
* ``*checkers: Callable[[Bot, Event], Awaitable[bool]]``: **异步** PermissionChecker
"""
self.checkers = set(checkers)
"""
:说明:
存储 ``PermissionChecker``
:类型:
* ``Set[Callable[[Bot, Event], Awaitable[bool]]]``
"""
async def __call__(self, bot: Bot, event: Event) -> bool: async def __call__(self, bot: Bot, event: Event) -> bool:
"""
:说明:
检查是否满足某个权限
:参数:
* ``bot: Bot``: Bot 对象
* ``event: Event``: Event 对象
:返回:
- ``bool``
"""
if not self.checkers: if not self.checkers:
return True return True
results = await asyncio.gather( results = await asyncio.gather(
@ -25,13 +55,13 @@ class Permission:
def __or__(self, other: Union["Permission", def __or__(self, other: Union["Permission",
PermissionChecker]) -> "Permission": PermissionChecker]) -> "Permission":
checkers = [*self.checkers] checkers = self.checkers.copy()
if isinstance(other, Permission): if isinstance(other, Permission):
checkers.extend(other.checkers) checkers |= other.checkers
elif asyncio.iscoroutinefunction(other): elif asyncio.iscoroutinefunction(other):
checkers.append(other) checkers.add(other) # type: ignore
else: else:
checkers.append(run_sync(other)) checkers.add(run_sync(other))
return Permission(*checkers) return Permission(*checkers)
@ -52,12 +82,31 @@ async def _metaevent(bot: Bot, event: Event) -> bool:
MESSAGE = Permission(_message) MESSAGE = Permission(_message)
"""
- **说明**: 匹配任意 ``message`` 类型事件仅在需要同时捕获不同类型事件时使用优先使用 message type Matcher
"""
NOTICE = Permission(_notice) NOTICE = Permission(_notice)
"""
- **说明**: 匹配任意 ``notice`` 类型事件仅在需要同时捕获不同类型事件时使用优先使用 notice type Matcher
"""
REQUEST = Permission(_request) REQUEST = Permission(_request)
"""
- **说明**: 匹配任意 ``request`` 类型事件仅在需要同时捕获不同类型事件时使用优先使用 request type Matcher
"""
METAEVENT = Permission(_metaevent) METAEVENT = Permission(_metaevent)
"""
- **说明**: 匹配任意 ``meta_event`` 类型事件仅在需要同时捕获不同类型事件时使用优先使用 meta_event type Matcher
"""
def USER(*user: int, perm: Permission = Permission()): def USER(*user: int, perm: Permission = Permission()):
"""
:说明:
在白名单内且满足 perm
:参数:
* ``*user: int``: 白名单
* ``perm: Permission``: 需要同时满足的权限
"""
async def _user(bot: Bot, event: Event) -> bool: async def _user(bot: Bot, event: Event) -> bool:
return event.type == "message" and event.user_id in user and await perm( return event.type == "message" and event.user_id in user and await perm(
@ -86,9 +135,21 @@ async def _private_other(bot: Bot, event: Event) -> bool:
PRIVATE = Permission(_private) PRIVATE = Permission(_private)
"""
- **说明**: 匹配任意私聊消息类型事件
"""
PRIVATE_FRIEND = Permission(_private_friend) PRIVATE_FRIEND = Permission(_private_friend)
"""
- **说明**: 匹配任意好友私聊消息类型事件
"""
PRIVATE_GROUP = Permission(_private_group) PRIVATE_GROUP = Permission(_private_group)
"""
- **说明**: 匹配任意群临时私聊消息类型事件
"""
PRIVATE_OTHER = Permission(_private_other) PRIVATE_OTHER = Permission(_private_other)
"""
- **说明**: 匹配任意其他私聊消息类型事件
"""
async def _group(bot: Bot, event: Event) -> bool: async def _group(bot: Bot, event: Event) -> bool:
@ -111,9 +172,25 @@ async def _group_owner(bot: Bot, event: Event) -> bool:
GROUP = Permission(_group) GROUP = Permission(_group)
"""
- **说明**: 匹配任意群聊消息类型事件
"""
GROUP_MEMBER = Permission(_group_member) GROUP_MEMBER = Permission(_group_member)
"""
- **说明**: 匹配任意群员群聊消息类型事件
\:\:\:warning 警告
该权限通过 event.sender 进行判断且不包含管理员以及群主
\:\:\:
"""
GROUP_ADMIN = Permission(_group_admin) GROUP_ADMIN = Permission(_group_admin)
"""
- **说明**: 匹配任意群管理员群聊消息类型事件
"""
GROUP_OWNER = Permission(_group_owner) GROUP_OWNER = Permission(_group_owner)
"""
- **说明**: 匹配任意群主群聊消息类型事件
"""
async def _superuser(bot: Bot, event: Event) -> bool: async def _superuser(bot: Bot, event: Event) -> bool:
@ -121,4 +198,10 @@ async def _superuser(bot: Bot, event: Event) -> bool:
SUPERUSER = Permission(_superuser) SUPERUSER = Permission(_superuser)
"""
- **说明**: 匹配任意超级用户消息类型事件
"""
EVERYBODY = MESSAGE EVERYBODY = MESSAGE
"""
- **说明**: 匹配任意消息类型事件
"""

View File

@ -1,5 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
规则
====
每个 ``Matcher`` 拥有一个 ``Rule`` 其中是 **异步** ``RuleChecker`` 的集合只有当所有 ``RuleChecker`` 检查结果为 ``True`` 时继续运行
\:\:\:tip 提示
``RuleChecker`` 既可以是 async function 也可以是 sync function
\:\:\:
"""
import re import re
import asyncio import asyncio
@ -10,28 +20,62 @@ from pygtrie import CharTrie
from nonebot import get_driver from nonebot import get_driver
from nonebot.log import logger from nonebot.log import logger
from nonebot.utils import run_sync from nonebot.utils import run_sync
from nonebot.typing import Bot, Any, Dict, Event, Union, Tuple, NoReturn, RuleChecker from nonebot.typing import Bot, Any, Dict, Event, Union, Tuple, NoReturn, Callable, Awaitable, RuleChecker
class Rule: class Rule:
"""
:说明:
``Matcher`` 规则类当事件传递时 ``Matcher`` 运行前进行检查
:示例:
.. code-block:: python
Rule(async_function) & sync_function
# 等价于
from nonebot.utils import run_sync
Rule(async_function, run_sync(sync_function))
"""
__slots__ = ("checkers",) __slots__ = ("checkers",)
def __init__(self, *checkers: RuleChecker) -> None: def __init__(
self.checkers = list(checkers) self, *checkers: Callable[[Bot, Event, dict],
Awaitable[bool]]) -> None:
"""
:参数:
* ``*checkers: Callable[[Bot, Event, dict], Awaitable[bool]]``: **异步** RuleChecker
"""
self.checkers = set(checkers)
"""
:说明:
存储 ``RuleChecker``
:类型:
* ``Set[Callable[[Bot, Event, dict], Awaitable[bool]]]``
"""
async def __call__(self, bot: Bot, event: Event, state: dict) -> bool: async def __call__(self, bot: Bot, event: Event, state: dict) -> bool:
"""
:说明:
检查是否符合所有规则
:参数:
* ``bot: Bot``: Bot 对象
* ``event: Event``: Event 对象
* ``state: dict``: 当前 State
:返回:
- ``bool``
"""
results = await asyncio.gather( results = await asyncio.gather(
*map(lambda c: c(bot, event, state), self.checkers)) *map(lambda c: c(bot, event, state), self.checkers))
return all(results) return all(results)
def __and__(self, other: Union["Rule", RuleChecker]) -> "Rule": def __and__(self, other: Union["Rule", RuleChecker]) -> "Rule":
checkers = [*self.checkers] checkers = self.checkers.copy()
if isinstance(other, Rule): if isinstance(other, Rule):
checkers.extend(other.checkers) checkers |= other.checkers
elif asyncio.iscoroutinefunction(other): elif asyncio.iscoroutinefunction(other):
checkers.append(other) checkers.add(other) # type: ignore
else: else:
checkers.append(run_sync(other)) checkers.add(run_sync(other))
return Rule(*checkers) return Rule(*checkers)
def __or__(self, other) -> NoReturn: def __or__(self, other) -> NoReturn:
@ -111,19 +155,29 @@ class TrieRule:
def startswith(msg: str) -> Rule: def startswith(msg: str) -> Rule:
TrieRule.add_prefix(msg, (msg,)) """
:说明:
匹配消息开头
:参数:
* ``msg: str``: 消息开头字符串
"""
async def _startswith(bot: Bot, event: Event, state: dict) -> bool: async def _startswith(bot: Bot, event: Event, state: dict) -> bool:
return msg in state["_prefix"] return event.plain_text.startswith(msg)
return Rule(_startswith) return Rule(_startswith)
def endswith(msg: str) -> Rule: def endswith(msg: str) -> Rule:
TrieRule.add_suffix(msg, (msg,)) """
:说明:
匹配消息结尾
:参数:
* ``msg: str``: 消息结尾字符串
"""
async def _endswith(bot: Bot, event: Event, state: dict) -> bool: async def _endswith(bot: Bot, event: Event, state: dict) -> bool:
return msg in state["_suffix"] return event.plain_text.endswith(msg)
return Rule(_endswith) return Rule(_endswith)

44
nonebot/sched.py Normal file
View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
计划任务
========
计划任务使用第三方库 `APScheduler`_ 使用文档请参考 `APScheduler使用文档`_
.. _APScheduler:
https://github.com/agronholm/apscheduler
.. _APScheduler使用文档:
https://apscheduler.readthedocs.io/en/latest/
"""
try:
from apscheduler.schedulers.asyncio import AsyncIOScheduler
except ImportError:
AsyncIOScheduler = None
if AsyncIOScheduler:
scheduler = AsyncIOScheduler()
"""
:类型:
``Optional[apscheduler.schedulers.asyncio.AsyncIOScheduler]``
:说明:
当可选依赖 ``APScheduler`` 未安装时``scheduler`` None
使用 ``pip install nonebot[scheduler]`` 安装可选依赖
:常用示例:
.. code-block:: python
from nonebot import scheduler
@scheduler.scheduled_job("cron", hour="*/2", id="xxx", args=[1], kwargs={arg2: 2})
async def run_every_2_hour(arg1, arg2):
pass
scheduler.add_job(run_every_day_from_program_start, "interval", days=1, id="xxx")
"""
else:
scheduler = None

View File

@ -46,7 +46,7 @@ def overrides(InterfaceClass: object):
Driver = TypeVar("Driver", bound="BaseDriver") Driver = TypeVar("Driver", bound="BaseDriver")
""" """
:类型: `BaseDriver` :类型: ``BaseDriver``
:说明: :说明:
@ -54,7 +54,7 @@ Driver = TypeVar("Driver", bound="BaseDriver")
""" """
WebSocket = TypeVar("WebSocket", bound="BaseWebSocket") WebSocket = TypeVar("WebSocket", bound="BaseWebSocket")
""" """
:类型: `BaseWebSocket` :类型: ``BaseWebSocket``
:说明: :说明:
@ -63,7 +63,7 @@ WebSocket = TypeVar("WebSocket", bound="BaseWebSocket")
Bot = TypeVar("Bot", bound="BaseBot") Bot = TypeVar("Bot", bound="BaseBot")
""" """
:类型: `BaseBot` :类型: ``BaseBot``
:说明: :说明:
@ -71,7 +71,7 @@ Bot = TypeVar("Bot", bound="BaseBot")
""" """
Event = TypeVar("Event", bound="BaseEvent") Event = TypeVar("Event", bound="BaseEvent")
""" """
:类型: `BaseEvent` :类型: ``BaseEvent``
:说明: :说明:
@ -79,7 +79,7 @@ Event = TypeVar("Event", bound="BaseEvent")
""" """
Message = TypeVar("Message", bound="BaseMessage") Message = TypeVar("Message", bound="BaseMessage")
""" """
:类型: `BaseMessage` :类型: ``BaseMessage``
:说明: :说明:
@ -87,7 +87,7 @@ Message = TypeVar("Message", bound="BaseMessage")
""" """
MessageSegment = TypeVar("MessageSegment", bound="BaseMessageSegment") MessageSegment = TypeVar("MessageSegment", bound="BaseMessageSegment")
""" """
:类型: `BaseMessageSegment` :类型: ``BaseMessageSegment``
:说明: :说明:
@ -97,7 +97,7 @@ MessageSegment = TypeVar("MessageSegment", bound="BaseMessageSegment")
PreProcessor = Callable[[Bot, Event, dict], Union[Awaitable[None], PreProcessor = Callable[[Bot, Event, dict], Union[Awaitable[None],
Awaitable[NoReturn]]] Awaitable[NoReturn]]]
""" """
:类型: `Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]` :类型: ``Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]``
:说明: :说明:
@ -106,7 +106,7 @@ PreProcessor = Callable[[Bot, Event, dict], Union[Awaitable[None],
Matcher = TypeVar("Matcher", bound="MatcherClass") Matcher = TypeVar("Matcher", bound="MatcherClass")
""" """
:类型: `Matcher` :类型: ``Matcher``
:说明: :说明:
@ -114,15 +114,15 @@ Matcher = TypeVar("Matcher", bound="MatcherClass")
""" """
Rule = TypeVar("Rule", bound="RuleClass") Rule = TypeVar("Rule", bound="RuleClass")
""" """
:类型: `Rule` :类型: ``Rule``
:说明: :说明:
Rule 即判断是否响应事件的处理类内部存储 RuleChecker 返回全为 True 则响应事件 Rule 即判断是否响应事件的处理类内部存储 RuleChecker 返回全为 True 则响应事件
""" """
RuleChecker = Callable[[Bot, Event, dict], Awaitable[bool]] RuleChecker = Callable[[Bot, Event, dict], Union[bool, Awaitable[bool]]]
""" """
:类型: `Callable[[Bot, Event, dict], Awaitable[bool]]` :类型: ``Callable[[Bot, Event, dict], Union[bool, Awaitable[bool]]]``
:说明: :说明:
@ -130,15 +130,15 @@ RuleChecker = Callable[[Bot, Event, dict], Awaitable[bool]]
""" """
Permission = TypeVar("Permission", bound="PermissionClass") Permission = TypeVar("Permission", bound="PermissionClass")
""" """
:类型: `Permission` :类型: ``Permission``
:说明: :说明:
Permission 即判断是否响应消息的处理类内部存储 PermissionChecker 返回只要有一个 True 则响应消息 Permission 即判断是否响应消息的处理类内部存储 PermissionChecker 返回只要有一个 True 则响应消息
""" """
PermissionChecker = Callable[[Bot, Event], Awaitable[bool]] PermissionChecker = Callable[[Bot, Event], Union[bool, Awaitable[bool]]]
""" """
:类型: `Callable[[Bot, Event], Awaitable[bool]]` :类型: ``Callable[[Bot, Event], Union[bool, Awaitable[bool]]]``
:说明: :说明:
@ -147,7 +147,7 @@ PermissionChecker = Callable[[Bot, Event], Awaitable[bool]]
Handler = Callable[[Bot, Event, dict], Union[Awaitable[None], Handler = Callable[[Bot, Event, dict], Union[Awaitable[None],
Awaitable[NoReturn]]] Awaitable[NoReturn]]]
""" """
:类型: `Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]` :类型: ``Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]``
:说明: :说明:
@ -156,7 +156,7 @@ Handler = Callable[[Bot, Event, dict], Union[Awaitable[None],
ArgsParser = Callable[[Bot, Event, dict], Union[Awaitable[None], ArgsParser = Callable[[Bot, Event, dict], Union[Awaitable[None],
Awaitable[NoReturn]]] Awaitable[NoReturn]]]
""" """
:类型: `Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]` :类型: ``Callable[[Bot, Event, dict], Union[Awaitable[None], Awaitable[NoReturn]]]``
:说明: :说明:

View File

@ -10,6 +10,14 @@ from nonebot.typing import Any, Callable, Awaitable, overrides
def run_sync(func: Callable[..., Any]) -> Callable[..., Awaitable[Any]]: def run_sync(func: Callable[..., Any]) -> Callable[..., Awaitable[Any]]:
"""
:说明:
一个用于包装 sync function async function 的装饰器
:参数:
* ``func: Callable[..., Any]``: 被装饰的同步函数
:返回:
- ``Callable[..., Awaitable[Any]]``
"""
@wraps(func) @wraps(func)
async def _wrapper(*args: Any, **kwargs: Any) -> Any: async def _wrapper(*args: Any, **kwargs: Any) -> Any:
@ -22,6 +30,10 @@ def run_sync(func: Callable[..., Any]) -> Callable[..., Awaitable[Any]]:
class DataclassEncoder(json.JSONEncoder): class DataclassEncoder(json.JSONEncoder):
"""
:说明:
在JSON序列化 ``Message`` (List[Dataclass]) 时使用的 ``JSONEncoder``
"""
@overrides(json.JSONEncoder) @overrides(json.JSONEncoder)
def default(self, o): def default(self, o):

713
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,10 +20,14 @@
}, },
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@vuepress/plugin-back-to-top": "^1.3.1", "@vuepress/plugin-back-to-top": "^1.5.4",
"@vuepress/plugin-medium-zoom": "^1.3.1", "@vuepress/plugin-medium-zoom": "^1.5.4",
"vuepress": "^1.3.1", "vuepress": "^1.5.4",
"vuepress-plugin-versioning": "^4.5.0", "vuepress-plugin-versioning": "^4.5.0",
"vuepress-theme-titanium": "^4.5.1" "vuepress-theme-titanium": "^4.5.1"
},
"dependencies": {
"vuetify": "^2.3.10",
"wowjs": "^1.1.3"
} }
} }

33
poetry.lock generated
View File

@ -197,7 +197,7 @@ description = "Chromium HSTS Preload list as a Python package and updated daily"
name = "hstspreload" name = "hstspreload"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
version = "2020.8.18" version = "2020.9.15"
[package.source] [package.source]
reference = "aliyun" reference = "aliyun"
@ -338,14 +338,14 @@ description = "Python logging made (stupidly) simple"
name = "loguru" name = "loguru"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "0.5.1" version = "0.5.2"
[package.dependencies] [package.dependencies]
colorama = ">=0.3.4" colorama = ">=0.3.4"
win32-setctime = ">=1.0.0" win32-setctime = ">=1.0.0"
[package.extras] [package.extras]
dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=4.3.20)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.3b0)"] dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.10b0)", "isort (>=5.1.1)"]
[package.source] [package.source]
reference = "aliyun" reference = "aliyun"
@ -427,7 +427,7 @@ description = "Pygments is a syntax highlighting package written in Python."
name = "pygments" name = "pygments"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "2.6.1" version = "2.7.1"
[package.source] [package.source]
reference = "aliyun" reference = "aliyun"
@ -620,10 +620,9 @@ unify = "*"
yapf = "*" yapf = "*"
[package.source] [package.source]
reference = "1438d33cbeaab0230c9f7e33bd059eb9f57c86d6" reference = "792133d3bb15b956e5150c158371eb71f5670844"
type = "git" type = "git"
url = "https://github.com/nonebot/sphinx-markdown-builder.git" url = "https://github.com/nonebot/sphinx-markdown-builder.git"
[[package]] [[package]]
category = "dev" category = "dev"
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
@ -861,7 +860,7 @@ marker = "sys_platform == \"win32\""
name = "win32-setctime" name = "win32-setctime"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "1.0.1" version = "1.0.2"
[package.extras] [package.extras]
dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
@ -885,10 +884,12 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple" url = "https://mirrors.aliyun.com/pypi/simple"
[extras] [extras]
full = []
scheduler = ["apscheduler"] scheduler = ["apscheduler"]
[metadata] [metadata]
content-hash = "2e8f1fc9fcb89a528ecbebbf0f2315abf39e3de8eb40c133b91085a784e49173" content-hash = "3a0f821e8ecef5548fa95e88cee11ff12910d08bed1633d8d03280f65d0bc1bf"
lock-version = "1.0"
python-versions = "^3.7" python-versions = "^3.7"
[metadata.files] [metadata.files]
@ -941,8 +942,8 @@ hpack = [
{file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"},
] ]
hstspreload = [ hstspreload = [
{file = "hstspreload-2020.8.18-py3-none-any.whl", hash = "sha256:5e3b6b2376c6f412086ee21cdd29cd5e0af5b28c967e5f1f026323d0f31dc84b"}, {file = "hstspreload-2020.9.15-py3-none-any.whl", hash = "sha256:c09f02dd4b7e3953a8353836aea964b868b22905bdb6d8932ca4b273df0f820f"},
{file = "hstspreload-2020.8.18.tar.gz", hash = "sha256:13cf2e9fcd064cd81d220432de9a66dd7e4f10862a03574c45e5f61fc522f312"}, {file = "hstspreload-2020.9.15.tar.gz", hash = "sha256:059fc2ead7bb83ea9e85d06c1afc7112413f1d2a81e6448bd276af6bced035ac"},
] ]
html2text = [ html2text = [
{file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"}, {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"},
@ -987,8 +988,8 @@ jinja2 = [
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
] ]
loguru = [ loguru = [
{file = "loguru-0.5.1-py3-none-any.whl", hash = "sha256:e5d362a43cd2fc2da63551d79a6830619c4d5b3a8b976515748026f92f351b61"}, {file = "loguru-0.5.2-py3-none-any.whl", hash = "sha256:a5e5e196b9958feaf534ac2050171d16576bae633074ce3e73af7dda7e9a58ae"},
{file = "loguru-0.5.1.tar.gz", hash = "sha256:70201d5fce26da89b7a5f168caa2bb674e06b969829f56737db1d6472e53e7c3"}, {file = "loguru-0.5.2.tar.gz", hash = "sha256:5aecbf13bc8e2f6e5a5d0475460a345b44e2885464095ea7de44e8795857ad33"},
] ]
markupsafe = [ markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
@ -1053,8 +1054,8 @@ pydash = [
{file = "pydash-4.8.0.tar.gz", hash = "sha256:546afa043ed1defa3122383bebe8b7072f43554ccc5f0c4360638f99e5ed7327"}, {file = "pydash-4.8.0.tar.gz", hash = "sha256:546afa043ed1defa3122383bebe8b7072f43554ccc5f0c4360638f99e5ed7327"},
] ]
pygments = [ pygments = [
{file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, {file = "Pygments-2.7.1-py3-none-any.whl", hash = "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998"},
{file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, {file = "Pygments-2.7.1.tar.gz", hash = "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7"},
] ]
pygtrie = [ pygtrie = [
{file = "pygtrie-2.3.3.tar.gz", hash = "sha256:2204dbd95584f67821da5b3771c4305ac5585552b3230b210f1f05322608db2c"}, {file = "pygtrie-2.3.3.tar.gz", hash = "sha256:2204dbd95584f67821da5b3771c4305ac5585552b3230b210f1f05322608db2c"},
@ -1178,8 +1179,8 @@ websockets = [
{file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"},
] ]
win32-setctime = [ win32-setctime = [
{file = "win32_setctime-1.0.1-py3-none-any.whl", hash = "sha256:568fd636c68350bcc54755213fe01966fe0a6c90b386c0776425944a0382abef"}, {file = "win32_setctime-1.0.2-py3-none-any.whl", hash = "sha256:02b4c5959ca0b195f45c98115826c6e8a630b7cf648e724feaab1a5aa6250640"},
{file = "win32_setctime-1.0.1.tar.gz", hash = "sha256:b47e5023ec7f0b4962950902b15bc56464a380d869f59d27dbf9ab423b23e8f9"}, {file = "win32_setctime-1.0.2.tar.gz", hash = "sha256:47aa7c43548c1fc0a4f026d1944b748b37036df116c7c4cf908e82638d854313"},
] ]
yapf = [ yapf = [
{file = "yapf-0.30.0-py2.py3-none-any.whl", hash = "sha256:3abf61ba67cf603069710d30acbc88cfe565d907e16ad81429ae90ce9651e0c9"}, {file = "yapf-0.30.0-py2.py3-none-any.whl", hash = "sha256:3abf61ba67cf603069710d30acbc88cfe565d907e16ad81429ae90ce9651e0c9"},

View File

@ -1,12 +1,12 @@
[tool.poetry] [tool.poetry]
name = "nonebot" name = "nonebot2"
version = "2.0.0" version = "2.0.0a1"
description = "An asynchronous python bot framework." description = "An asynchronous python bot framework."
authors = ["yanyongyu <yanyongyu_1@126.com>"] authors = ["yanyongyu <yanyongyu_1@126.com>"]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
homepage = "https://docs.nonebot.dev/" homepage = "https://docs.nonebot.dev/"
repository = "https://github.com/nonebot/nonebot" repository = "https://github.com/nonebot/nonebot2"
documentation = "https://docs.nonebot.dev/" documentation = "https://docs.nonebot.dev/"
keywords = ["bot", "qq", "qqbot", "mirai", "coolq"] keywords = ["bot", "qq", "qqbot", "mirai", "coolq"]
classifiers = [ classifiers = [
@ -17,27 +17,33 @@ classifiers = [
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3" "Programming Language :: Python :: 3"
] ]
packages = [
{ include = "nonebot" }
]
include = ["nonebot/py.typed"] include = ["nonebot/py.typed"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = "^3.7"
httpx = "^0.13.3" httpx = "^0.13.3"
loguru = "^0.5.1"
pygtrie = "^2.3.3" pygtrie = "^2.3.3"
fastapi = "^0.58.1" fastapi = "^0.58.1"
uvicorn = "^0.11.5" uvicorn = "^0.11.5"
pydantic = { extras = ["dotenv"], version = "^1.6.1" } pydantic = { extras = ["dotenv"], version = "^1.6.1" }
apscheduler = { version = "^3.6.3", optional = true } apscheduler = { version = "^3.6.3", optional = true }
# nonebot-test = { version = "^0.1.0", optional = true } # nonebot-test = { version = "^0.1.0", optional = true }
# nb-cli = { version="^0.1.0", optional = true }
loguru = "^0.5.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
yapf = "^0.30.0" yapf = "^0.30.0"
sphinx = "^3.1.1" sphinx = "^3.1.1"
sphinx-markdown-builder = { git = "https://github.com/nonebot/sphinx-markdown-builder.git" } sphinx-markdown-builder = { git = "https://github.com/nonebot/sphinx-markdown-builder.git" }
[tool.poetry.extras] [tool.poetry.extras]
# cli = ["nb-cli"]
# test = ["nonebot-test"] # test = ["nonebot-test"]
scheduler = ["apscheduler"] scheduler = ["apscheduler"]
full = ["nb-cli", "nonebot-test", "scheduler"]
[[tool.poetry.source]] [[tool.poetry.source]]
name = "aliyun" name = "aliyun"

View File

@ -6,4 +6,5 @@ DEBUG=true
COMMAND_START=["", "/", "#"] COMMAND_START=["", "/", "#"]
COMMAND_SEP=["/", "."] COMMAND_SEP=["/", "."]
CUSTOM_CONFIG={"custom": 1} CUSTOM_CONFIG1=config in env
CUSTOM_CONFIG3=

View File

@ -16,14 +16,19 @@ logger.add("error.log",
level="ERROR", level="ERROR",
format=default_format) format=default_format)
nonebot.init() nonebot.init(custom_config2="config on init")
app = nonebot.get_asgi() app = nonebot.get_asgi()
# load builtin plugin # load builtin plugin
nonebot.load_plugin("nonebot.plugins.base") nonebot.load_builtin_plugins()
# load local plugins # load local plugins
nonebot.load_plugins("test_plugins") nonebot.load_plugins("test_plugins")
# modify some config / config depends on loaded configs
config = nonebot.get_driver().config
config.custom_config3 = config.custom_config1
config.custom_config4 = "New custom config"
if __name__ == "__main__": if __name__ == "__main__":
nonebot.run(app="bot:app") nonebot.run(app="bot:app")

View File

@ -1,11 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from nonebot.rule import to_me
from nonebot.typing import Event from nonebot.typing import Event
from nonebot.plugin import on_message from nonebot.plugin import on_message
from nonebot.adapters.cqhttp import Bot from nonebot.adapters.cqhttp import Bot
test_message = on_message(state={"default": 1}) test_message = on_message(to_me(), state={"default": 1})
@test_message.handle() @test_message.handle()

View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from nonebot.rule import to_me
from nonebot.typing import Event
from nonebot.plugin import on_startswith
from nonebot.adapters.cqhttp import Bot
from nonebot.permission import GROUP_ADMIN
test_command = on_startswith("hello", to_me(), permission=GROUP_ADMIN)
@test_command.handle()
async def test_handler(bot: Bot, event: Event, state: dict):
await test_command.finish("hello")

View File

@ -0,0 +1,24 @@
from nonebot import on_command
from nonebot.rule import to_me
from nonebot.typing import Bot, Event
weather = on_command("天气", rule=to_me(), priority=1)
@weather.handle()
async def handle_first_receive(bot: Bot, event: Event, state: dict):
args = str(event.message).strip() # 首次发送命令时跟随的参数,例:/天气 上海则args为上海
print(f"==={args}===")
if args:
state["city"] = args # 如果用户发送了参数则直接赋值
@weather.got("city", prompt="你想查询哪个城市的天气呢?")
async def handle_city(bot: Bot, event: Event, state: dict):
city = state["city"]
if city not in ["上海", "北京"]:
await weather.reject("你想查询的城市暂不支持,请重新输入!")
# weather = await get_weather_from_xxx(city)
city_weather = "晴天"
# await bot.send(city_weather)
await weather.finish(city_weather)