【NoneBot2】第二章:基础插件编写指南第四节———人家才懒得理你呢

  1. 1. 第二章:基础插件编写指南第四节———人家才懒得理你呢
    1. 1.1. 本节前言
    2. 1.2. priority 及 事件阻断
      1. 1.2.1. priority
      2. 1.2.2. 简述事件阻断机制
      3. 1.2.3. 如何使用?
    3. 1.3. rule模块
    4. 1.4. permission

第二章:基础插件编写指南第四节———人家才懒得理你呢

本节中,将会学习 priority 优先级、简述事件阻断机制、permission 权限控制以及 rule 模块的使用。

本节前言

在接触到不少萌新开发者之后,我发现有不少新手的插件仅注重功能的实现,而不注重 “安全性” 方面的防范,导致机器人经常在群里被大家玩坏,导致账号风控甚至冻结。

我们首先来举几个例子,帮助大家了解一下为何我们要注重机器人的安全性:

  1. 你写了一个涩图插件,那么在无内鬼的开车群里可以火力全开,但是在其他群聊里面你就会面临 “正义执行” 的举报,因此我们就需要限制发图频率甚至关闭插件保平安。
  2. 你写了一个复读机插件,然后群里还有另外一个机器人也装了复读机插件,那么在触发条件之后这两个机器人很有可能就会互相刷屏,导致直接炸群。
  3. 你写了一个爬虫插件,爬取的数据量较大,如果反复触发会导致网站封禁你的ip,这时你就需要过滤掉部分恶意请求。
  4. 你写了一个群管插件,可以用于批量清人或禁言等敏感操作,这时你就需要设置仅由部分成员或管理员才能对机器人进行操作。

上述情况实质上的解决方法都是一种——屏蔽掉我们不希望被执行的请求,那么这章的工具将会是你的得力助手。


priority 及 事件阻断

priority

顾名思义,priority 为事件响应器优先级。在收到一条消息之后,nonebot会按照 升序 的方式逐一尝试使用每个事件响应器,直到事件被阻断(结束)或遍历完全部响应器。官方建议的 priority 取值范围为大于等于1的正整数

Tips:在nonebot的源码中,使用了 sorted() 函数对 priority 进行排序,也就是说理论上你加载的全部插件的 priorty 放到一个列表里后能够使用 sorted() 排序,那么理论上就不会报错。
这意味着你甚至可以传入不止大于等于1的正整数,你可以使用0、负数、浮点数甚至字符串(前提是全部 priority 都使用的是字符串)作为priority。但由于源码中某些重要事件的优先级为0小于等于0的优先级很有可能会导致不可知的问题,请务必不要这么做!请一定要按照官方的建议,使用大于等于1的正整数作为插件的优先级。

聪明的你可能要提出一些疑问了:“为什么这个 priority 感觉和安全性关系不大呢?”,实际上也正是这样,priority 更多的会被应用在插件间的流程控制上,而配合起事件阻断机制可以对消息进行筛选,将事件拦截在触发之前。

简述事件阻断机制

首先声明一点:这里仅是简述,可能会出现表述不严谨和过分简化的情况,请仅作为辅助理解。

在nonebot接收到事件并进行预处理之后,会将这个事件按照 priority 的顺序进行遍历,当以下情况之一发生的情况时,该事件将不会被继续处理:

  1. 有任意事件响应器发出了阻止事件传递信号时。
  2. 全部事件响应器均已传递完成,没有其他事件响应器可用。

也就是说,如果事件一直没有被阻断的话,可以被多个事件处理器接受并处理,相对的,如果事件被阻断了,后面的事件响应器即便能对该消息进行处理也不会被触发了。

举个例子:假设有人在群里发了张涩图,那么网速快的群友就能陆续收到这份涩图并开冲,然后被管理员 “OT警察” 看到了,认为影响不好,便撤回了涩图,那么后续的群友就无缘见到这张涩图了。

带入到nonebot的事件处理流程中,便是:机器人接收到消息后会生成一个事件,依次遍历事件响应器,优先级高一些的事件响应器可以更快的响应这个事件,直到某个事件响应器阻断了这个事件,那么后面的事件响应器就不会收到这个事件了。

目前,我们可以通过在事件响应器中添加 block 参数来指定该事件响应器是否会在执行完成后进行阻断,也可以在事件处理函数中使用 matcher.stop_propagation() 来直接阻断该事件

1
2
3
4
5
6
7
from nonebot import on_message
from nonebot.matcher import Matcher

foo = on_message(priority=1, block=False)
@foo.handle()
async def handle(matcher: Matcher):
matcher.stop_propagation()

Tips:

  1. matcher.stop_propagation() 会直接阻断该事件的传播,也会直接结束该事件的处理流程,类似于在函数中进行 return ,此后的代码将不会被执行。
  2. 不同的辅助函数其默认block状态并不相同,因此请务必注意其阻断状态不会影响其他事件响应器的正常执行。

如何使用?

聪明的你一定想到了:我们可以通过设置一个优先级最高的事件响应器来检查全部的事件,对于不符合规范的事件可用通过事件阻断机制进行屏蔽

注意:这种方法并不能拦截全部事件进行检查,而且相对来说功能也比较有限,在此仅作为示例展示,本人更加推荐使用 钩子函数 对事件进行 预处理 的写法,该种写法会在进阶教程中展示(如果我没咕咕咕的话)

实现起来也非常的简单,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from nonebot import on_message
from nonebot.matcher import Matcher
from nonebot.adapters.onebot.v11 import Event

def can_pass(message):
# 判断是否可以通过
if 1==1:
return True
else:
return False

bot_guard = on_message(priority=1, block=False) # block=False 即默认不阻断事件
@bot_guard.handle()
async def bot_guard_handle(matcher: Matcher,event: Event):
message = str(event.get_message()) # 获取用户所发的消息内容
if not can_pass(message): # 如果返回值是False则阻断事件
matcher.stop_propagation()

当然,在此基础上你可以添加依据群号、qq号、关键词和发言频率等一系列判断措施。


rule模块

官方文档:机器人在实际应用中,往往会接收到多种多样的事件类型,NoneBot2 提供了可自定义的匹配规则 ── Rule

正如官方文档的描述,rule模块可以在事件还未被处理,在响应器层面实现对消息的过滤。当然,你也可以在事件处理阶段对事件进行判断并且在不符合预期的情况下finish掉这个事件(就像上边的黑名单插件)。不过在事件响应器阶段对事件进行初步筛选可以大幅降低被触发的事件处理流程的数量,也就意味着能更加节约服务器资源,同时也可以减少无效log的数量,方便对日志进行阅读。

当然,使用rule模块的时候,一定要注意不要进行 事件处理,有可能会造成bug,如果需要涉及到事件处理流程的判断,我们还是可以通过强制结束事件来处理的,没必要讲全部判断都一股脑塞到rule中。个人建议的写法为在rule中仅进行简单的判断,进行初步筛选和过滤即可。

我们以判断QQ号为例展示一下使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
from nonebot import on_message
from nonebot.adapters.onebot.v11 import Event

async def user_checker(event: Event) -> bool:
if event.get_user_id() == "123123":
return True
elif event.get_user_id() == "321321":
return True
else:
return False

setu_sender = on_message(rule=user_checker)

不难发现,我们在用于检查的函数中,可以自由引用和事件处理函数中相同的依赖,也就是代码中的 event ,而且使用方法也和正常的处理流程中是一样的。

同时在rule模块中的Rule()能够支持将一个及以上的检查器(也就是user_checker)合并为一个整体进行检查,遵循规则(一否全否),具体代码实现可以参考官方文档合并匹配规则,在此不多阐述。


permission

在机器人的实际应用中,总有一些功能是不适宜在大庭广众下使用的,也有一些敏感的功能仅能供给给机器人拥有者(超级用户)使用的。在nonebot中,Permission就是解决这类问题的存在。

正如同其字面意思,Permission 的作用就是对用户身份进行辨认和过滤,仅允许通过判断的用户触发其所属的事件响应器。

注意:以下部分摘自官方文档

如同 Rule 一样,Permission 可以在定义事件响应器时添加 permission 参数来加以应用,这样 NoneBot2 会在事件响应时检测事件主体的权限。下面我们以 SUPERUSER 为例,对该机制的应用做一下介绍。

1
2
3
4
5
6
7
8
9
10
11
12
from nonebot.permission import SUPERUSER
from nonebot import on_command

matcher = on_command("测试超管", permission=SUPERUSER)

@matcher.handle()
async def _():
await matcher.send("超管命令测试成功")

@matcher.got("key1", "超管提问")
async def _():
await matcher.send("超管命令 got 成功")

在这段代码中,我们事件响应器指定了 SUPERUSER 这样一个权限,那么机器人只会响应超级管理员的 测试超管 命令,并且会响应该超级管理员的连续对话。

提示
在这里需要强调的是,PermissionRule 的表现并不相同, Rule 只会在初次响应时生效,在余下的对话中并没有限制事件;但是 Permission 会持续生效,在连续对话中一直对事件主体加以限制。

注意:以上部分摘自官方文档

在nonebot中,permission的权限分为两部分,由 nonebot.permission 提供的 SUPERUSER,以及适配器提供的权限,其中由 onebot.v11 提供的权限有以下几种

权限类型 匹配范围
PRIVATE 全部私聊
PRIVATE_FRIEND 私聊好友
PRIVATE_GROUP 群临时私聊
PRIVATE_OTHER 其他临时私聊
GROUP 全部群聊
GROUP_MEMBER 任意群员
GROUP_ADMIN 群管理
GROUP_OWNER 群主

如同 Rule 一样,Permission 也是由非负整数个 PermissionChecker 组成的,但只需其中一个返回 True 时就会匹配成功。

例如,我们想要匹配私聊群管理时,可以使用这种写法:

1
2
3
4
from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN,PRIVATE
from nonebot import on_command

matcher = on_command("setu", permission=PRIVATE|GROUP_ADMIN)

那么这个响应器将会对任意私聊或群聊中的管理员进行响应。