Add filter support

This commit is contained in:
Richard Chien
2016-12-08 21:58:49 +08:00
parent 7c5d7630ea
commit af431f17b1
7 changed files with 138 additions and 5 deletions

View File

@ -1,7 +1,35 @@
# QQBot # QQBot
此 QQBot 非彼 QQBot不是对 SmartQQ 的封装,而是基于开源的 [sjdy521/Mojo-Webqq](https://github.com/sjdy521/Mojo-Webqq) 实现的处理命令的逻辑 此 QQBot 非彼 QQBot不是对 SmartQQ 的封装,而是基于开源的 [sjdy521/Mojo-Webqq](https://github.com/sjdy521/Mojo-Webqq) 实现的对消息的自动处理程序,支持自定义插件
现在基本框架已经完成,不过还有部分基础性的命令没有实现,待完成之后,再来进行命令的扩充。 ## 如何部署
由于还没有完成,代码的各个部分、程序的功能等可能会变动比较频繁,此 README 先不详细写。目前可以参考 [编写命令](Write_Command.md) 来了解如何编写命令,因为命令是本程序的重要内容,所以这个文档会更新比较及时 推荐使用 Docker 部署,因为基本可以一键开启,如果你想手动运行,也可以参考第二个小标题「手动部署」
### 使用 Docker
本仓库根目录下的 `docker-compose.yml` 即为 Docker Compose 的配置文件,直接跑就行。如果你想对镜像进行修改,可以自行更改 Dockerfile 来构建或者继承已经构建好的镜像。
### 手动运行
首先需要运行 sjdy521/Mojo-Webqq具体见它的 GitHub 仓库的使用教程。然后运行:
```sh
pip install -r requirements.txt
python app.py
```
注意要求 Python 3.x。
## 插件
程序支持两种插件形式一种是过滤器Filter一种是命令Command。
本质上程序主体是一个 web app接受 sjdy521/Mojo-Webqq 的 POST 请求,从而收到消息。收到消息后,首先运行过滤器,按照优先级从大到小顺序运行 `filters` 目录中的 `.py` 文件中指定的过滤器函数,函数返回非 False 即表示不拦截消息,从而消息继续传给下一个过滤器,如果返回了 False则消息不再进行后续处理而直接抛弃。过滤器运行完之后会开始按照命令执行首先根据命令的开始标志判断有没有消息中有没有指定命令如果指定了则执行指定的命令如果没指定则看当前用户有没有开启交互式会话如果开启了会话则执行会话指定的命令否则使用默认的 fallback 命令。
过滤器和命令的使用场景区别:
- 过滤器:可用于消息的后台日志、频率控制、关键词分析,一般在使用者无意识的情况下进行;
- 命令:使用者有意识地想要使用某个给定的命令的功能。
关于过滤器和命令的细节,请参考 [编写过滤器](Write_Filter.md) 和 [编写命令](Write_Command.md)。

24
Write_Filter.md Normal file
View File

@ -0,0 +1,24 @@
# 编写过滤器
编写过滤器比较简单,只需要调用 `filter.py` 中的 `add_filter` 函数,传入过滤器函数和优先级,即可。
比如我们需要做一个消息拦截器,当匹配到消息中有不文明词汇,就发送一条警告,并拦截消息不让后续过滤器和命令处理,代码可能如下:
```python
from filter import add_filter
from commands import core
def _interceptor(ctx_msg):
if 'xxx' in ctx_msg.get('content', ''):
core.echo('请不要说脏话', ctx_msg)
return False
return True
add_filter(_interceptor, 100)
```
一般建议优先级设置为 0100 之间。
过滤器函数返回 True 表示让消息继续传递,返回 False 表示拦截消息。由于很多情况下可能不需要拦截,因此为了方便起见,将不返回值的情况(返回 None作为不拦截处理因此只要返回结果 is not False 就表示不拦截。

18
app.py
View File

@ -10,6 +10,7 @@ from config import config
from command import hub as cmdhub from command import hub as cmdhub
from command import CommandNotExistsError, CommandScopeError, CommandPermissionError from command import CommandNotExistsError, CommandScopeError, CommandPermissionError
from apiclient import client as api from apiclient import client as api
from filter import apply_filters
app = Flask(__name__) app = Flask(__name__)
@ -29,11 +30,13 @@ def _send_text(text, ctx_msg):
@app.route('/', methods=['POST']) @app.route('/', methods=['POST'])
def _index(): def _index():
ctx_msg = request.json ctx_msg = request.json
source = get_source(ctx_msg)
try: try:
if ctx_msg.get('msg_class') != 'recv': if ctx_msg.get('msg_class') != 'recv':
raise SkipException raise SkipException
if not apply_filters(ctx_msg):
raise SkipException
content = ctx_msg.get('content', '') content = ctx_msg.get('content', '')
source = get_source(ctx_msg)
if content.startswith('@'): if content.startswith('@'):
my_group_nick = ctx_msg.get('receiver') my_group_nick = ctx_msg.get('receiver')
if not my_group_nick: if not my_group_nick:
@ -91,6 +94,16 @@ def _index():
return '', 204 return '', 204
def _load_filters():
filter_mod_files = filter(
lambda filename: filename.endswith('.py') and not filename.startswith('_'),
os.listdir(get_filters_dir())
)
command_mods = [os.path.splitext(file)[0] for file in filter_mod_files]
for mod_name in command_mods:
importlib.import_module('filters.' + mod_name)
def _load_commands(): def _load_commands():
command_mod_files = filter( command_mod_files = filter(
lambda filename: filename.endswith('.py') and not filename.startswith('_'), lambda filename: filename.endswith('.py') and not filename.startswith('_'),
@ -106,5 +119,6 @@ def _load_commands():
if __name__ == '__main__': if __name__ == '__main__':
_load_filters()
_load_commands() _load_commands()
app.run(host=os.environ.get('HOST'), port=os.environ.get('PORT')) app.run(host=os.environ.get('HOST', '0.0.0.0'), port=os.environ.get('PORT', '8080'))

14
filter.py Normal file
View File

@ -0,0 +1,14 @@
_filters = []
def apply_filters(ctx_msg):
filters = sorted(_filters, key=lambda x: x[0], reverse=True)
for f in filters:
r = f[1](ctx_msg)
if r is False:
return False
return True
def add_filter(func, priority):
_filters.append((priority, func))

View File

@ -0,0 +1,39 @@
from datetime import datetime, timedelta
from cachetools import TTLCache as TTLDict
from filter import add_filter
from little_shit import get_target
from commands import core
_freq_count = TTLDict(maxsize=10000, ttl=2 * 60 * 60)
_max_message_count_per_hour = 150
def _limiter(ctx_msg):
target = get_target(ctx_msg)
if target not in _freq_count:
# First message of this target in 2 hours (_freq_count's ttl)
_freq_count[target] = (0, datetime.now())
count, last_check_dt = _freq_count[target]
now_dt = datetime.now()
delta = now_dt - last_check_dt
if delta >= timedelta(hours=1):
count = 0
last_check_dt = now_dt
if count >= _max_message_count_per_hour:
# Too many messages in this hour
core.echo('我们聊天太频繁啦,休息一会儿再聊吧~', ctx_msg)
count = -1
if count >= 0:
count += 1
_freq_count[target] = (count, last_check_dt)
return count >= 0
add_filter(_limiter, 100)

10
filters/message_logger.py Normal file
View File

@ -0,0 +1,10 @@
from filter import add_filter
def _log_message(ctx_msg):
print(ctx_msg.get('sender', '')
+ (('@' + ctx_msg.get('group')) if ctx_msg.get('type') == 'group_message' else '')
+ ': ' + ctx_msg.get('content'))
add_filter(_log_message, 1000)

View File

@ -16,6 +16,10 @@ def get_root_dir():
return os.path.split(os.path.realpath(__file__))[0] return os.path.split(os.path.realpath(__file__))[0]
def get_filters_dir():
return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), 'filters'))
def get_commands_dir(): def get_commands_dir():
return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), 'commands')) return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), 'commands'))