From af431f17b17f04be9c2480bc5601877eab808c08 Mon Sep 17 00:00:00 2001 From: Richard Chien Date: Thu, 8 Dec 2016 21:58:49 +0800 Subject: [PATCH] Add filter support --- README.md | 34 ++++++++++++++++++++++++++++--- Write_Filter.md | 24 ++++++++++++++++++++++ app.py | 18 +++++++++++++++-- filter.py | 14 +++++++++++++ filters/frequency_limiter.py | 39 ++++++++++++++++++++++++++++++++++++ filters/message_logger.py | 10 +++++++++ little_shit.py | 4 ++++ 7 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 Write_Filter.md create mode 100644 filter.py create mode 100644 filters/frequency_limiter.py create mode 100644 filters/message_logger.py diff --git a/README.md b/README.md index 1f26918b..ef70023c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,35 @@ # 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)。 diff --git a/Write_Filter.md b/Write_Filter.md new file mode 100644 index 00000000..71f9c818 --- /dev/null +++ b/Write_Filter.md @@ -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) +``` + +一般建议优先级设置为 0~100 之间。 + +过滤器函数返回 True 表示让消息继续传递,返回 False 表示拦截消息。由于很多情况下可能不需要拦截,因此为了方便起见,将不返回值的情况(返回 None)作为不拦截处理,因此只要返回结果 is not False 就表示不拦截。 diff --git a/app.py b/app.py index 3e4bd312..4f2a35d0 100644 --- a/app.py +++ b/app.py @@ -10,6 +10,7 @@ from config import config from command import hub as cmdhub from command import CommandNotExistsError, CommandScopeError, CommandPermissionError from apiclient import client as api +from filter import apply_filters app = Flask(__name__) @@ -29,11 +30,13 @@ def _send_text(text, ctx_msg): @app.route('/', methods=['POST']) def _index(): ctx_msg = request.json - source = get_source(ctx_msg) try: if ctx_msg.get('msg_class') != 'recv': raise SkipException + if not apply_filters(ctx_msg): + raise SkipException content = ctx_msg.get('content', '') + source = get_source(ctx_msg) if content.startswith('@'): my_group_nick = ctx_msg.get('receiver') if not my_group_nick: @@ -91,6 +94,16 @@ def _index(): 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(): command_mod_files = filter( lambda filename: filename.endswith('.py') and not filename.startswith('_'), @@ -106,5 +119,6 @@ def _load_commands(): if __name__ == '__main__': + _load_filters() _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')) diff --git a/filter.py b/filter.py new file mode 100644 index 00000000..bfbf2529 --- /dev/null +++ b/filter.py @@ -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)) diff --git a/filters/frequency_limiter.py b/filters/frequency_limiter.py new file mode 100644 index 00000000..ccb0a028 --- /dev/null +++ b/filters/frequency_limiter.py @@ -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) diff --git a/filters/message_logger.py b/filters/message_logger.py new file mode 100644 index 00000000..f626d557 --- /dev/null +++ b/filters/message_logger.py @@ -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) diff --git a/little_shit.py b/little_shit.py index 9a956a1f..3661d888 100644 --- a/little_shit.py +++ b/little_shit.py @@ -16,6 +16,10 @@ def get_root_dir(): 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(): return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), 'commands'))