在编写插件之前,首先我们需要了解一下插件的概念。
在 NoneBot 中,插件可以是 Python 的一个模块 module
,也可以是一个包 package
。NoneBot 会在导入时对这些模块或包做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合,可以进行有限制的插件间调用,NoneBot 能够正确解析插件间的依赖关系。
下面详细介绍两种插件的结构:
在合适的路径创建一个 .py
文件即可。例如在创建项目中创建的项目中,我们可以在 awesome_bot/plugins/
目录中创建一个文件 foo.py
。
这个时候它已经可以被称为一个插件了,尽管它还什么都没做。
在合适的路径创建一个文件夹,并在文件夹内创建文件 __init__.py
即可。例如在创建项目中创建的项目中,我们可以在 awesome_bot/plugins/
目录中创建一个文件夹 foo
,并在这个文件夹内创建一个文件 __init__.py
。
这个时候 foo
就是一个合法的 Python 包了,同时也是合法的 NoneBot 插件,插件内容可以在 __init__.py
中编写。
除了通过手动创建的方式以外,还可以通过 nb-cli 来创建插件,nb-cli 会为你在合适的位置创建一个模板包插件。
nb plugin create
我们在编写我们的插件之前,我们需要学会如何去加载插件
注意:
- 请勿在插件被加载前
import
插件模块,这会导致 NoneBot2 无法将其转换为插件而损失部分功能。
load_plugin
通过点分割模块名称来加载插件,通常用于加载单个插件或者是第三方插件。例如:
nonebot.load_plugin(“path.to.your.plugin”)
load_plugins
nonebot.load_plugins(“src/plugins”, “path/to/your/plugins”)
```
> 请注意,插件所在目录应该为相对机器人入口文件可导入的,例如与入口文件在同一目录下。
load_from_json
load_all_plugins
的 JSON 变种。通过读取 JSON 文件中的 plugins
字段和 plugin_dirs
字段进行加载。例如:
// plugin_config.json
{ “plugins”: [“path.to.your.plugin”], “plugin_dirs”: [“path/to/your/plugins”]}
```
```python
nonebot.load_from_json("plugin_config.json", encoding="utf-8")
> 如果 JSON 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](https://nb2.baka.icu/docs/tutorial/plugin/load-plugin#load_all_plugins) 方法自行读取配置来加载插件。
load_from_toml
load_all_plugins
的 TOML 变种。通过读取 TOML 文件中的 [tool.nonebot]
Table 中的 plugins
和 plugin_dirs
Array 进行加载。例如: # plugin_config.toml
[tool.nonebot]
plugins = ["path.to.your.plugin"]
plugin_dirs = ["path/to/your/plugins"]
```python
nonebot.load_from_toml(“plugin_config.toml”, encoding=“utf-8”)
```
load_builtin_plugin
load_plugin
的封装。例如:
nonebot.load_builtin_plugin(“echo”)
```
我们可以使用配置文件加载
倘若 plugin_a
, plugin_b
均需被加载, 且 plugin_b
插件需要导入 plugin_a
才可运行, 可以在 plugin_b
利用 require
方法来确保插件加载, 同时可以直接 import
导入 plugin_a
,进行跨插件访问。
from nonebot import require
require('plugin_a')
import plugin_a
不用
require
方法也可以进行跨插件访问,但需要保证插件已加载
通常,插件可以从配置文件中读取自己的配置项,但是由于额外的全局配置项没有预先定义的问题,导致开发时编辑器无法提示字段与类型,以及运行时没有对配置项直接进行检查。那么就需要一种方式来规范定义插件配置项。
我们定义一个配置模型:
在 NoneBot2 中,我们使用强大高效的 Pydantic 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如,我们可以定义一个配置模型包含一个 string 类型的配置项:
config.py
from pydantic import BaseModel, Extra
class Config(BaseModel, extra=Extra.ignore):
token: str
定义完成配置模型后,我们可以在插件加载时获取全局配置,导入插件自身的配置模型:
__init__.py
from nonebot import get_driver
from .config import Config
plugin_config = Config.parse_obj(get_driver().config)
事件响应器(Matcher
)是对接收到的事件进行响应的基本单元,所有的事件响应器都继承自 Matcher
基类。为了方便开发者编写插件,NoneBot2 在 nonebot.plugin
模块中为插件开发定义了一些辅助函数。首先,让我们来了解一下 Matcher
由哪些部分组成。
事件响应器类型(type
)
事件响应器的类型即是该响应器所要响应的事件类型,只有在接收到的事件类型与该响应器的类型相同时,才会触发该响应器。如果类型留空,该响应器将会响应所有类型的事件。
NoneBot 内置了四种主要类型:meta_event
、message
、notice
、request
。通常情况下,协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应,可以自行定义新的类型。
事件匹配规则(rule
)
事件响应器的匹配规则是一个 Rule
对象,它是一系列 checker
的集合,当所有的 checker
都返回 True
时,才会触发该响应器。
事件触发权限(permission
)
事件响应器的触发权限是一个 Permission
对象,它也是一系列 checker
的集合,当其中一个 checker
返回 True
时,就会触发该响应器。
优先级(priority
)
事件响应器的优先级代表事件响应器的执行顺序
阻断(block
)
当有任意事件响应器发出了阻止事件传递信号时,该事件将不再会传递给下一优先级,直接结束处理。
NoneBot 内置的事件响应器中,所有非 command 规则的 message 类型的事件响应器都会阻断事件传递,其他则不会。
在部分情况中,可以使用 matcher.stop_propagation() 方法动态阻止事件传播,该方法需要 handler 在参数中获取 matcher 实例后调用方法。
有效期(expire_time/temp
)
事件响应器可以设置有效期,当事件响应器超过有效期时,将会被移除:
创建事件响应器的辅助函数有以下几种:
on
: 创建任何类型的事件响应器。on_metaevent
: 创建元事件响应器。on_message
: 创建消息事件响应器。on_request
: 创建请求事件响应器。on_notice
: 创建通知事件响应器。on_startswith
: 创建消息开头匹配事件响应器。on_endswith
: 创建消息结尾匹配事件响应器。on_fullmatch
: 创建消息完全匹配事件响应器。on_keyword
: 创建消息关键词匹配事件响应器。on_command
: 创建命令消息事件响应器。on_shell_command
: 创建 shell 命令消息事件响应器。on_regex
: 创建正则表达式匹配事件响应器。CommandGroup
: 创建具有共同命令名称前缀的命令组。MatcherGroup
: 创建具有共同参数的响应器组。其中:
on_metaevent
on_message
on_request
on_notice
函数都是在 on
的基础上添加了对应的事件类型 type
on_startswith
on_endswith
on_fullmatch
on_keyword
on_command
on_shell_command
on_regex
函数都是在 on_message
的基础上添加了对应的匹配规则 rule
我们已经定义了事件响应器,然后,我们将会为事件响应器填充处理流程。
在事件响应器中,事件处理流程由一个或多个处理依赖组成,每个处理依赖都是一个 Dependent
,下面介绍如何添加一个处理依赖,具体可以查看官网:https://nb2.baka.icu/docs/advanced/di/dependency-injection。
from nonebot import on_command
from nonebot.params import Depends # 1.引用 Depends
from nonebot.adapters.onebot.v11 import MessageEvent
test = on_command("123")
async def depend(event: MessageEvent): # 2.编写依赖函数
return {"uid": event.get_user_id(), "nickname": event.sender.nickname}
@test.handle()
async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项
print(x["uid"], x["nickname"])
如注释所言,可以用三步来说明依赖注入的使用过程:
引用
Depends
编写依赖函数。依赖函数和普通的事件处理函数并无区别,同样可以接收
bot
,event
,state
等参数,你可以把它当作一个普通的事件处理函数,但是去除了装饰器(没有使用matcher.handle()
等来装饰),并且可以返回任何类型的值在这里我们接受了
event
,并以onebot
的MessageEvent
作为类型标注,返回一个新的字典,包括uid
和nickname
两个键值在事件处理函数中声明依赖项。依赖项必须要
Depends
包裹依赖函数作为默认值
matcher = on_message()
@matcher.handle()
async def handle_func():
# do something here
如上方示例所示,我们使用 matcher
响应器的 handle
装饰器装饰了一个函数 handle_func
。handle_func
函数会被自动转换为 Dependent
对象,并被添加到 matcher
的事件处理流程中。
在 handle_func
函数中,我们可以编写任何事件响应逻辑,如:操作数据库,发送消息等。
matcher = on_message()
@matcher.receive("id")
async def handle_func(e: Event = Received("id")):
# do something here
receive
装饰器与 handle
装饰器一样,可以装饰一个函数添加到事件响应器的事件处理流程中。但与 handle
装饰器不同的是,receive
装饰器会中断当前事件处理流程,等待接收一个新的事件,就像是会话状态等待用户一个新的事件。可以接收的新的事件类型取决于事件响应器的 type
更新值以及 permission
更新值,可以通过自定义更新方法来控制会话响应(如进行非消息交互、多人会话、跨群会话等)。
matcher = on_message()
@matcher.got("key", prompt="Key?")
async def handle_func(key: Message = Arg()):
# do something here
got
装饰器与 receive
装饰器一样,会中断当前事件处理流程,等待接收一个新的事件。但与 receive
装饰器不同的是,got
装饰器用于接收一条消息,并且可以控制是否向用户发送询问 prompt
等,更贴近于对话形式会话。
got
装饰器接受一个参数 key
和一个可选参数 prompt
,当 key
不存在时,会向用户发送 prompt
消息,并等待用户回复。
matcher = on_message(
handlers=[handle_func, or_dependent]
)
同时,这个流程需要配合上下文信息来使用,这里就不再多讲,请查看官方文档:https://nb2.baka.icu/docs/tutorial/plugin/create-handler#%E8%8E%B7%E5%8F%96%E4%B8%8A%E4%B8%8B%E6%96%87%E4%BF%A1%E6%81%AF
send
向用户回复一条消息。回复的方式或途径由协议适配器自行实现。
可以是 str
、Message
、MessageSegment
或 MessageTemplate
。
这个操作等同于使用 bot.send(event, message, **kwargs)
但不需要自行传入 event
。
@matcher.handle()
async def _():
await matcher.send("Hello world!")
finish
向用户回复一条消息(可选),并立即结束当前事件的整个处理流程。
参数与 send
相同。
@matcher.handle()
async def _():
await matcher.finish("Hello world!")
# something never run
...
pause
向用户回复一条消息(可选),并立即结束当前事件处理依赖并等待接收一个新的事件后进入下一个事件处理依赖。
类似于 receive
的行为但可以根据事件来决定是否接收新的事件。
@matcher.handle()
async def _():
if serious:
await matcher.pause("Confirm?")
@matcher.handle()
async def _():
...
reject
向用户回复一条消息(可选),并立即结束当前事件处理依赖并等待接收一个新的事件后再次执行当前事件处理依赖。
通常用于拒绝当前 receive
接收的事件或 got
接收的参数(如:不符合格式或标准)。
@matcher.got("arg")
async def _(arg: str = ArgPlainText()):
if not is_valid(arg):
await matcher.reject("Invalid arg!"
reject_arg
向用户回复一条消息(可选),并立即结束当前事件处理依赖并等待接收一个新的事件后再次执行当前事件处理依赖。
用于拒绝指定 got
接收的参数,通常在嵌套装饰器时使用。
@matcher.got("a")
@matcher.got("b")
async def _(a: str = ArgPlainText(), b: str = ArgPlainText()):
if a not in b:
await matcher.reject_arg("a", "Invalid a!")
reject_receive
向用户回复一条消息(可选),并立即结束当前事件处理依赖并等待接收一个新的事件后再次执行当前事件处理依赖。
用于拒绝指定 receive
接收的事件,通常在嵌套装饰器时使用。
@matcher.receive("a")
@matcher.receive("b")
async def _(a: Event = Received("a"), b: Event = Received("b")):
if a.get_user_id() != b.get_user_id():
await matcher.reject_receive("a")
skip
立即结束当前事件处理依赖,进入下一个事件处理依赖。
通常在子依赖中使用,用于跳过当前事件处理依赖的执行。
async def dependency(matcher: Matcher):
matcher.skip()
@matcher.handle()
async def _(sub=Depends(dependency)):
# never run
...
get_receive
获取一个 receive
接收的事件。
set_receive
设置/覆盖一个 receive
接收的事件。
get_last_receive
获取最近一次 receive
接收的事件。
get_arg
获取一个 got
接收的参数。
set_arg
设置/覆盖一个 got
接收的参数。
stop_propagation
阻止事件向更低优先级的事件响应器传播。
@foo.handle()
async def _(matcher: Matcher):
matcher.stop_propagation()
这里,我们制作一个获取文学信息的插件
在data_source.py
中:
from dataclasses import dataclass
from typing import Tuple, Optional, Protocol
from httpx import AsyncClient
from nonebot.adapters.onebot.v11 import Message
import re
# 文学类的接口
# 历史上的今天:
async def _history():
"""历史的今天的数据获取"""
async with AsyncClient() as session:
resp = await session.get("https://yuanxiapi.cn/api/history/?format=json")
r = resp.json()
if r.get("code", '300') == '200':
day = r["day"]
content = "\n".join([f"{i + 1}. " + k for i, k in enumerate(r["content"])])
ret = f"{day}\n\n{content}"
else:
ret = "数据获取失败!"
return Message(ret)
# 每日简报
async def _brief():
"""每日简报的获取"""
async with AsyncClient() as client:
try:
resp = await client.get("https://api.2xb.cn/zaob?format=json")
resp = resp.json()
url = resp['imageUrl']
except:
resp = await client.get("http://bjb.yunwj.top/php/tp/lj.php")
url = re.findall('"tp":"(?P.*?)"' , resp.text)[0]
pic_ti = f"[CQ:image,file={url}]"
return Message(pic_ti)
# 鸡汤
async def _soup1():
"""获取随机语录"""
async with AsyncClient() as client:
try:
resp = await client.get("http://api.zhaoge.fun/api/rshy.php")
sten = resp.text.split()[2]
return Message(sten)
except Exception as e:
print(e)
return Message("获取失败,请重新尝试")
# 毒鸡汤
async def _soup2():
"""获取毒鸡汤"""
async with AsyncClient(follow_redirects=True) as client:
resp = await client.get("https://api.sunweihu.com/api/yan/api.php?charset=utf-8&encode=json")
if resp.is_success:
ret = resp.json()["text"]
else:
ret = "获取失败!接口出现问题!"
return Message(ret)
# 诗词
async def _poem():
"""获取随机诗词"""
async with AsyncClient() as client:
ret = await client.get("https://v2.jinrishici.com/token")
data = ret.json()
if data.get("status") == "success":
token = data["data"]
else:
return Message("获取失败,请重新尝试!")
headers = {
"X-User-Token": token
}
ret = await client.get("https://v2.jinrishici.com/sentence", headers=headers)
data = ret.json()
if data.get("status") == "success":
content = data["data"]["content"]
return Message(content)
else:
return Message("获取失败,请重新尝试!")
# 段子
async def _paragraph():
"""获取段子"""
async with AsyncClient(follow_redirects=True) as client:
resp = await client.get("https://yuanxiapi.cn/api/Aword/")
if resp.is_success:
ret = resp.json()["duanju"]
else:
ret = "获取失败!"
return Message(ret)
class Func(Protocol):
# 声明为函数
async def __call__(self) -> Optional[Message]:
...
@dataclass
class Source:
# 用来存储数据的类
name: str
keywords: Tuple[str, ...]
func: Func
sources = [
Source("history", ("历史", "历史上的今天"), _history),
Source("brief", ("简报", "每日简报"), _brief),
Source("soup", ("人生语录", "鸡汤"), _soup1),
Source("bad_soup", ("随机一言", "毒鸡汤"), _soup2),
Source("poem", ("诗词", "诗"), _poem),
Source("paragrah", ("段子"), _paragraph)
]
在__init__.py
中:
from nonebot import on_command
from nonebot.typing import T_Handler
from .data_source import Source, sources
from nonebot.matcher import Matcher
from nonebot.plugin import PluginMetadata
"""
获取关于文学的所有内容,后面会不断完善
"""
__plugin_meta__ = PluginMetadata(
name="文学",
description="这个插件连接着关于文学的所有的内容!",
usage="/历史 /简报 /鸡汤 /毒鸡汤 /诗 /段子",
)
def create_matchers():
def create_handler(source: Source) -> T_Handler:
async def handler(matcher: Matcher):
res = None
try:
res = await source.func()
if not res:
res = "获取数据失败"
except Exception as e:
print(e)
res = "出错了,请稍后再试"
await matcher.finish(res)
return handler
for source in sources:
on_command(
source.keywords[0], aliases=set(source.keywords), block=True, priority=12
).append_handler(create_handler(source))
create_matchers()
这里使用的是数据类的方式来批量存储命令和函数,实现批量添加