基于Nonebot2搭建QQ机器人(二):插件使用

文章目录

  • Nonebot2创建插件
    • 1、 插件简介
      • 1.1 模块插件(单文件形式)
      • 1.2 包插件(文件夹形式)
      • 1.3 创建插件
    • 2、 加载插件
      • 2.1 直接加载
      • 2.2 跨域加载
    • 3、 插件配置
      • 3.1 创建模型
      • 3.2 导入配置
    • 4、 事件响应
      • 4.1 事件响应器
      • 4.2 创建响应器
      • 4.3 事件处理
        • 4.3.1 事件依赖
        • 4.3.2 添加流程
          • 4.3.2.1 handler
          • 4.3.2.2 receive
          • 4.3.2.3 got
          • 4.3.2.4 直接添加
      • 4.4 事件响应器操作
    • 5、 插件案例

Nonebot2创建插件

1、 插件简介

在编写插件之前,首先我们需要了解一下插件的概念。

在 NoneBot 中,插件可以是 Python 的一个模块 module,也可以是一个包 package 。NoneBot 会在导入时对这些模块或包做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合,可以进行有限制的插件间调用,NoneBot 能够正确解析插件间的依赖关系。

下面详细介绍两种插件的结构:

1.1 模块插件(单文件形式)

在合适的路径创建一个 .py 文件即可。例如在创建项目中创建的项目中,我们可以在 awesome_bot/plugins/ 目录中创建一个文件 foo.py

这个时候它已经可以被称为一个插件了,尽管它还什么都没做。

1.2 包插件(文件夹形式)

在合适的路径创建一个文件夹,并在文件夹内创建文件 __init__.py 即可。例如在创建项目中创建的项目中,我们可以在 awesome_bot/plugins/ 目录中创建一个文件夹 foo,并在这个文件夹内创建一个文件 __init__.py

这个时候 foo 就是一个合法的 Python 包了,同时也是合法的 NoneBot 插件,插件内容可以在 __init__.py 中编写。

1.3 创建插件

除了通过手动创建的方式以外,还可以通过 nb-cli 来创建插件,nb-cli 会为你在合适的位置创建一个模板包插件。

nb plugin create

2、 加载插件

2.1 直接加载

我们在编写我们的插件之前,我们需要学会如何去加载插件

注意:

  • 请勿在插件被加载前 import 插件模块,这会导致 NoneBot2 无法将其转换为插件而损失部分功能。
  1. load_plugin

    • 通过点分割模块名称来加载插件,通常用于加载单个插件或者是第三方插件。例如:

    nonebot.load_plugin(“path.to.your.plugin”)

    
    
  2. load_plugins

    • 加载传入插件目录中的所有插件,通常用于加载一系列本地编写的插件。例如:

nonebot.load_plugins(“src/plugins”, “path/to/your/plugins”)
```

> 请注意,插件所在目录应该为相对机器人入口文件可导入的,例如与入口文件在同一目录下。 
  1. load_from_json

    • 通过 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) 方法自行读取配置来加载插件。 
  1. load_from_toml
    • 通过 TOML 文件加载插件,是 load_all_plugins 的 TOML 变种。通过读取 TOML 文件中的 [tool.nonebot] Table 中的 pluginsplugin_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”)
```

  1. load_builtin_plugin

    • 加载一个内置插件,是 load_plugin 的封装。例如:

nonebot.load_builtin_plugin(“echo”)
```

我们可以使用配置文件加载

2.2 跨域加载

倘若 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 方法也可以进行跨插件访问,但需要保证插件已加载

3、 插件配置

通常,插件可以从配置文件中读取自己的配置项,但是由于额外的全局配置项没有预先定义的问题,导致开发时编辑器无法提示字段与类型,以及运行时没有对配置项直接进行检查。那么就需要一种方式来规范定义插件配置项。

3.1 创建模型

我们定义一个配置模型:

在 NoneBot2 中,我们使用强大高效的 Pydantic 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如,我们可以定义一个配置模型包含一个 string 类型的配置项:

config.py

from pydantic import BaseModel, Extra


class Config(BaseModel, extra=Extra.ignore):
    token: str

3.2 导入配置

定义完成配置模型后,我们可以在插件加载时获取全局配置,导入插件自身的配置模型:

__init__.py

from nonebot import get_driver

from .config import Config

plugin_config = Config.parse_obj(get_driver().config)

4、 事件响应

4.1 事件响应器

事件响应器(Matcher)是对接收到的事件进行响应的基本单元,所有的事件响应器都继承自 Matcher 基类。为了方便开发者编写插件,NoneBot2 在 nonebot.plugin 模块中为插件开发定义了一些辅助函数。首先,让我们来了解一下 Matcher 由哪些部分组成。

  1. 事件响应器类型(type

    事件响应器的类型即是该响应器所要响应的事件类型,只有在接收到的事件类型与该响应器的类型相同时,才会触发该响应器。如果类型留空,该响应器将会响应所有类型的事件。
    NoneBot 内置了四种主要类型:meta_eventmessagenoticerequest。通常情况下,协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应,可以自行定义新的类型。

  2. 事件匹配规则(rule

    事件响应器的匹配规则是一个 Rule 对象,它是一系列 checker 的集合,当所有的 checker 都返回 True 时,才会触发该响应器。

  3. 事件触发权限(permission

    事件响应器的触发权限是一个 Permission 对象,它也是一系列 checker 的集合,当其中一个 checker 返回 True 时,就会触发该响应器。

  4. 优先级(priority

    事件响应器的优先级代表事件响应器的执行顺序

  5. 阻断(block

    当有任意事件响应器发出了阻止事件传递信号时,该事件将不再会传递给下一优先级,直接结束处理。
    NoneBot 内置的事件响应器中,所有非 command 规则的 message 类型的事件响应器都会阻断事件传递,其他则不会。
    在部分情况中,可以使用 matcher.stop_propagation() 方法动态阻止事件传播,该方法需要 handler 在参数中获取 matcher 实例后调用方法。

  6. 有效期(expire_time/temp

    事件响应器可以设置有效期,当事件响应器超过有效期时,将会被移除:

    • temp 属性:配置事件响应器在下一次响应之后移除。
    • expire_time 属性:配置事件响应器在指定时间之后移除。

4.2 创建响应器

创建事件响应器的辅助函数有以下几种:

  1. on: 创建任何类型的事件响应器。
  2. on_metaevent: 创建元事件响应器。
  3. on_message: 创建消息事件响应器。
  4. on_request: 创建请求事件响应器。
  5. on_notice: 创建通知事件响应器。
  6. on_startswith: 创建消息开头匹配事件响应器。
  7. on_endswith: 创建消息结尾匹配事件响应器。
  8. on_fullmatch: 创建消息完全匹配事件响应器。
  9. on_keyword: 创建消息关键词匹配事件响应器。
  10. on_command: 创建命令消息事件响应器。
  11. on_shell_command: 创建 shell 命令消息事件响应器。
  12. on_regex: 创建正则表达式匹配事件响应器。
  13. CommandGroup: 创建具有共同命令名称前缀的命令组。
  14. 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

4.3 事件处理

我们已经定义了事件响应器,然后,我们将会为事件响应器填充处理流程。

4.3.1 事件依赖

在事件响应器中,事件处理流程由一个或多个处理依赖组成,每个处理依赖都是一个 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"])

如注释所言,可以用三步来说明依赖注入的使用过程:

  1. 引用 Depends

  2. 编写依赖函数。依赖函数和普通的事件处理函数并无区别,同样可以接收 bot, event, state 等参数,你可以把它当作一个普通的事件处理函数,但是去除了装饰器(没有使用 matcher.handle() 等来装饰),并且可以返回任何类型的值

    在这里我们接受了 event,并以 onebotMessageEvent 作为类型标注,返回一个新的字典,包括 uidnickname 两个键值

  3. 在事件处理函数中声明依赖项。依赖项必须要 Depends 包裹依赖函数作为默认值

4.3.2 添加流程

4.3.2.1 handler
matcher = on_message()


@matcher.handle()
async def handle_func():
    # do something here

如上方示例所示,我们使用 matcher 响应器的 handle 装饰器装饰了一个函数 handle_funchandle_func 函数会被自动转换为 Dependent 对象,并被添加到 matcher 的事件处理流程中。

handle_func 函数中,我们可以编写任何事件响应逻辑,如:操作数据库,发送消息等。

4.3.2.2 receive
matcher = on_message()


@matcher.receive("id")
async def handle_func(e: Event = Received("id")):
    # do something here

receive 装饰器与 handle 装饰器一样,可以装饰一个函数添加到事件响应器的事件处理流程中。但与 handle 装饰器不同的是,receive 装饰器会中断当前事件处理流程,等待接收一个新的事件,就像是会话状态等待用户一个新的事件。可以接收的新的事件类型取决于事件响应器的 type 更新值以及 permission 更新值,可以通过自定义更新方法来控制会话响应(如进行非消息交互、多人会话、跨群会话等)。

4.3.2.3 got
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 消息,并等待用户回复。

4.3.2.4 直接添加
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

4.4 事件响应器操作

  1. send

    向用户回复一条消息。回复的方式或途径由协议适配器自行实现。

    可以是 strMessageMessageSegmentMessageTemplate

    这个操作等同于使用 bot.send(event, message, **kwargs) 但不需要自行传入 event

    @matcher.handle()
    async def _():
        await matcher.send("Hello world!")
    
  2. finish

    向用户回复一条消息(可选),并立即结束当前事件的整个处理流程。

    参数与 send 相同。

    @matcher.handle()
    async def _():
        await matcher.finish("Hello world!")
        # something never run
        ...
    
  3. pause

    向用户回复一条消息(可选),并立即结束当前事件处理依赖并等待接收一个新的事件后进入下一个事件处理依赖。

    类似于 receive 的行为但可以根据事件来决定是否接收新的事件。

    @matcher.handle()
    async def _():
        if serious:
            await matcher.pause("Confirm?")
    
    
    @matcher.handle()
    async def _():
        ...
    
  4. reject

    向用户回复一条消息(可选),并立即结束当前事件处理依赖并等待接收一个新的事件后再次执行当前事件处理依赖。

    通常用于拒绝当前 receive 接收的事件或 got 接收的参数(如:不符合格式或标准)。

    @matcher.got("arg")
    async def _(arg: str = ArgPlainText()):
        if not is_valid(arg):
            await matcher.reject("Invalid arg!"
    
  5. 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!")
    
  6. 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")
    
  7. skip

    立即结束当前事件处理依赖,进入下一个事件处理依赖。

    通常在子依赖中使用,用于跳过当前事件处理依赖的执行。

    async def dependency(matcher: Matcher):
        matcher.skip()
    
    @matcher.handle()
    async def _(sub=Depends(dependency)):
        # never run
        ...
    
  8. get_receive

    获取一个 receive 接收的事件。

  9. set_receive

    设置/覆盖一个 receive 接收的事件。

  10. get_last_receive

    获取最近一次 receive 接收的事件。

  11. get_arg

    获取一个 got 接收的参数。

  12. set_arg

    设置/覆盖一个 got 接收的参数。

  13. stop_propagation

    阻止事件向更低优先级的事件响应器传播。

    @foo.handle()
    async def _(matcher: Matcher):
        matcher.stop_propagation()
    

5、 插件案例

这里,我们制作一个获取文学信息的插件

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()

这里使用的是数据类的方式来批量存储命令和函数,实现批量添加

你可能感兴趣的:(#,机器人,python,开发语言)