diff --git a/app.py b/app.py index a8128e1f..cd24c74a 100644 --- a/app.py +++ b/app.py @@ -1,31 +1,29 @@ import os -import importlib from flask import Flask, request -from little_shit import SkipException, get_filters_dir +from little_shit import SkipException, load_plugins from filter import apply_filters +from msg_src_adapter import get_adapter app = Flask(__name__) -@app.route('/qq/', methods=['POST']) -def _handle_qq_message(): +@app.route('//', methods=['POST'], strict_slashes=False) +def _handle_via_account(via: str, login_id: str): ctx_msg = request.json - ctx_msg['via'] = 'qq' - return _main(ctx_msg) - - -@app.route('/wx/', methods=['POST']) -def _handle_wx_message(): - ctx_msg = request.json - ctx_msg['via'] = 'wx' + ctx_msg['via'] = via + ctx_msg['login_id'] = login_id return _main(ctx_msg) def _main(ctx_msg: dict): try: - if ctx_msg.get('post_type') != 'receive_message': + adapter = get_adapter(ctx_msg.get('via'), ctx_msg.get('login_id')) + if not adapter: + raise SkipException + ctx_msg = adapter.unitize_context(ctx_msg) + if ctx_msg.get('post_type') != 'message': raise SkipException if not apply_filters(ctx_msg): raise SkipException @@ -36,16 +34,7 @@ def _main(ctx_msg: dict): 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) - - if __name__ == '__main__': - _load_filters() + load_plugins('msg_src_adapters') + load_plugins('filters') app.run(host=os.environ.get('HOST', '0.0.0.0'), port=os.environ.get('PORT', '8080')) diff --git a/command.py b/command.py index 335744c6..bf53107f 100644 --- a/command.py +++ b/command.py @@ -1,9 +1,8 @@ import functools import re -import os -from apiclient import client as api -from little_shit import SkipException, get_command_name_separators, get_command_args_separators +from little_shit import get_command_name_separators, get_command_args_separators +from msg_src_adapter import get_adapter_by_ctx _command_name_seps = get_command_name_separators() _command_args_seps = get_command_args_separators() @@ -117,13 +116,15 @@ class CommandRegistry: if command_name in self.command_map: func = self.command_map[command_name] if not self._check_scope(func, ctx_msg): - msg_type = ctx_msg.get('type') - if msg_type == 'group_message': + msg_type = ctx_msg.get('msg_type') + if msg_type == 'group': msg_type_str = '群组消息' - elif msg_type == 'discuss_message': + elif msg_type == 'discuss': msg_type_str = '讨论组消息' - else: + elif msg_type == 'private': msg_type_str = '私聊消息' + else: + msg_type_str = '未知来源消息' raise CommandScopeError(msg_type_str) if not self._check_permission(func, ctx_msg): raise CommandPermissionError @@ -140,13 +141,13 @@ class CommandRegistry: """ allowed_msg_type = set() if func.allow_group: - allowed_msg_type.add('group_message') + allowed_msg_type.add('group') if func.allow_discuss: - allowed_msg_type.add('discuss_message') + allowed_msg_type.add('discuss') if func.allow_private: - allowed_msg_type.add('friend_message') + allowed_msg_type.add('private') - if ctx_msg.get('type') in allowed_msg_type: + if ctx_msg.get('msg_type') in allowed_msg_type: return True return False @@ -160,39 +161,23 @@ class CommandRegistry: :param ctx_msg: context message :return: permitted or not """ + adapter = get_adapter_by_ctx(ctx_msg) + if adapter.is_sender_superuser(ctx_msg): + return True # Superuser is the BIG BOSS - def check(b): - if not b: - raise SkipException + if func.superuser_only: + return False - try: - if func.superuser_only: - raise SkipException - if ctx_msg.get('type') == 'group_message' and ctx_msg.get('via') == 'qq': - allowed_roles = {'owner', 'admin', 'member'} - if func.group_admin_only: - allowed_roles.intersection_update({'owner', 'admin'}) - if func.group_owner_only: - allowed_roles.intersection_update({'owner'}) - groups = list(filter( - lambda g: str(g.get('id')) == ctx_msg.get('group_id'), - api.get_group_info(ctx_msg).json() - )) - if len(groups) <= 0 or 'member' not in groups[0]: - # This is strange, not likely happens - raise SkipException + if ctx_msg.get('msg_type') == 'group': + # TODO: 在酷 Q 测试一下 + allowed_roles = {'owner', 'admin', 'member'} + if func.group_admin_only: + allowed_roles.intersection_update({'owner', 'admin'}) + if func.group_owner_only: + allowed_roles.intersection_update({'owner'}) - members = list(filter( - lambda m: str(m.get('id')) == ctx_msg.get('sender_id'), - groups[0].get('member') - )) - if len(members) <= 0 or members[0].get('role') not in allowed_roles: - # This is strange, not likely happens - raise SkipException - except SkipException: - if ctx_msg.get('via') == 'qq' and ctx_msg.get('sender_uid') != os.environ.get('QQ_SUPER_USER'): - return False - elif ctx_msg.get('via') == 'wx' and ctx_msg.get('sender_account') != os.environ.get('WX_SUPER_USER'): + role = adapter.get_sender_group_role(ctx_msg) + if role not in allowed_roles: return False # Still alive, let go diff --git a/commands/ai.py b/commands/ai.py index f395db94..1f1f7235 100644 --- a/commands/ai.py +++ b/commands/ai.py @@ -4,7 +4,6 @@ import requests from command import CommandRegistry from commands import core from little_shit import get_source -from apiclient import client as api __registry__ = cr = CommandRegistry() @@ -33,15 +32,15 @@ def tuling123(args_text, ctx_msg, internal=False): reply = '腊鸡图灵机器人出问题了,先不管他,过会儿再玩他' core.echo(reply, ctx_msg) - -@cr.register('xiaoice', '小冰') -def xiaoice(args_text, ctx_msg, internal=False): - resp = api.wx_consult(account='xiaoice-ms', content=args_text) - if resp: - json = resp.json() - if json and json.get('reply'): - reply = json['reply'] - core.echo(reply, ctx_msg, internal) - return reply - core.echo('小冰没有回复,请稍后再试', ctx_msg, internal) - return None +# TODO: 加入微信消息源之后修改 +# @cr.register('xiaoice', '小冰') +# def xiaoice(args_text, ctx_msg, internal=False): +# resp = api.wx_consult(account='xiaoice-ms', content=args_text) +# if resp: +# json = resp.json() +# if json and json.get('reply'): +# reply = json['reply'] +# core.echo(reply, ctx_msg, internal) +# return reply +# core.echo('小冰没有回复,请稍后再试', ctx_msg, internal) +# return None diff --git a/commands/core.py b/commands/core.py index 36d3040f..e519ba6b 100644 --- a/commands/core.py +++ b/commands/core.py @@ -1,5 +1,5 @@ from command import CommandRegistry -from apiclient import client as api +from msg_src_adapter import get_adapter_by_ctx __registry__ = cr = CommandRegistry() @@ -9,7 +9,10 @@ def echo(args_text, ctx_msg, internal=False): if internal: return None else: - return api.send_message(args_text, ctx_msg) + return get_adapter_by_ctx(ctx_msg).send_message( + target=ctx_msg, + content=args_text + ) @cr.register('help', '帮助', '用法', '使用帮助', '使用指南', '使用说明', '使用方法', '怎么用') diff --git a/commands/natural_language.py b/commands/natural_language.py index 1a04a8e3..684fcc78 100644 --- a/commands/natural_language.py +++ b/commands/natural_language.py @@ -1,15 +1,12 @@ -import os -import importlib - from command import CommandRegistry from commands import core from nl_processor import parse_potential_commands -from little_shit import get_nl_processors_dir, get_fallback_command_after_nl_processors +from little_shit import load_plugins, get_fallback_command_after_nl_processors from command import hub as cmdhub def _init(): - _load_processors() + load_plugins('nl_processors') __registry__ = cr = CommandRegistry(init_func=_init) @@ -41,13 +38,3 @@ def process(args_text, ctx_msg): core.echo('暂时无法理解你的意思。\n' '由于自然语言识别还非常不完善,建议使用命令来精确控制我。\n' '如需帮助请发送「使用帮助」。', ctx_msg) - - -def _load_processors(): - processor_mod_files = filter( - lambda filename: filename.endswith('.py') and not filename.startswith('_'), - os.listdir(get_nl_processors_dir()) - ) - command_mods = [os.path.splitext(file)[0] for file in processor_mod_files] - for mod_name in command_mods: - importlib.import_module('nl_processors.' + mod_name) diff --git a/commands/scheduler.py b/commands/scheduler.py index defeb8db..60b95e41 100644 --- a/commands/scheduler.py +++ b/commands/scheduler.py @@ -1,6 +1,6 @@ import os import re -from functools import reduce, wraps +from functools import wraps import pytz import requests diff --git a/commands/sudo.py b/commands/sudo.py index c5da62d8..b61749cc 100644 --- a/commands/sudo.py +++ b/commands/sudo.py @@ -26,22 +26,21 @@ def test(_, ctx_msg): @cr.register('block') @cr.restrict(full_command_only=True, superuser_only=True) -@split_arguments(maxsplit=2) +@split_arguments(maxsplit=1) def block(_, ctx_msg, argv=None): def _send_error_msg(): - core.echo('参数不正确。\n\n正确使用方法:\nsudo.block wx|qq ', ctx_msg) + core.echo('参数不正确。\n\n正确使用方法:\nsudo.block ', ctx_msg) - if len(argv) != 2: + if len(argv) != 1: _send_error_msg() return - via, account = argv + account = argv[0] # Get a target using a fake context message target = get_target({ - 'via': via, - 'type': 'friend_message', - 'sender_uid': account, - 'sender_account': account + 'via': 'default', + 'msg_type': 'private', + 'sender_id': account }) if not target: @@ -65,31 +64,28 @@ def block_list(_, ctx_msg, internal=False): if internal: return blocked_targets if blocked_targets: - # `t[1:]` to reply user account, without target prefix 'p'. - # This is a shit code, and should be changed later sometime. - core.echo('已屏蔽的用户:\n' + ', '.join([t[1:] for t in blocked_targets]), ctx_msg) + core.echo('已屏蔽的用户:\n' + ', '.join(blocked_targets), ctx_msg) else: core.echo('还没有屏蔽过用户', ctx_msg) @cr.register('unblock') @cr.restrict(full_command_only=True, superuser_only=True) -@split_arguments(maxsplit=2) +@split_arguments(maxsplit=1) def unblock(_, ctx_msg, argv=None): def _send_error_msg(): - core.echo('参数不正确。\n\n正确使用方法:\nsudo.unblock wx|qq ', ctx_msg) + core.echo('参数不正确。\n\n正确使用方法:\nsudo.unblock ', ctx_msg) - if len(argv) != 2: + if len(argv) != 1: _send_error_msg() return - via, account = argv + account = argv[0] # Get a target using a fake context message target = get_target({ - 'via': via, - 'type': 'friend_message', - 'sender_uid': account, - 'sender_account': account + 'via': 'default', + 'msg_type': 'private', + 'sender_id': account }) if not target: diff --git a/config.py b/config.py index f85bd7ea..3855e87a 100644 --- a/config.py +++ b/config.py @@ -5,4 +5,19 @@ config = { 'command_name_separators': ('->', '::', '/'), # Regex 'command_args_start_flags': (',', ':', ',', ', ', ':', ': '), # Regex 'command_args_separators': (',', ','), # Regex + + 'message_sources': [ + { + 'via': 'mojo_weixin', + 'login_id': 'rcdevtest', + 'superuser_id': 'richard_chien_0', + 'api_url': 'http://127.0.0.1:5001/openwx', + }, + { + 'via': 'mojo_webqq', + 'login_id': '3281334718', + 'superuser_id': '1002647525', + 'api_url': 'http://127.0.0.1:5000/openqq', + } + ] } diff --git a/filters/command_dispatcher_0.py b/filters/command_dispatcher_0.py index e721f0c5..2b8d9222 100644 --- a/filters/command_dispatcher_0.py +++ b/filters/command_dispatcher_0.py @@ -1,6 +1,5 @@ import re import sys -import importlib import interactive from filter import as_filter @@ -14,20 +13,6 @@ _command_start_flags = get_command_start_flags() _command_args_start_flags = get_command_args_start_flags() -def _load_commands(): - command_mod_files = filter( - lambda filename: filename.endswith('.py') and not filename.startswith('_'), - os.listdir(get_commands_dir()) - ) - command_mods = [os.path.splitext(file)[0] for file in command_mod_files] - for mod_name in command_mods: - cmd_mod = importlib.import_module('commands.' + mod_name) - try: - cmdhub.add_registry(mod_name, cmd_mod.__registry__) - except AttributeError: - print('Failed to load command module "' + mod_name + '.py".', file=sys.stderr) - - @as_filter(priority=0) def _dispatch_command(ctx_msg): try: @@ -77,4 +62,12 @@ def _dispatch_command(ctx_msg): core.echo('这个命令不支持' + se.msg_type + '哦~', ctx_msg) -_load_commands() +def _add_registry_mod_cb(mod): + mod_name = mod.__name__.split('.')[1] + try: + cmdhub.add_registry(mod_name, mod.__registry__) + except AttributeError: + print('Failed to load command module "' + mod_name + '.py".', file=sys.stderr) + + +load_plugins('commands', module_callback=_add_registry_mod_cb) diff --git a/filters/intercept_some_message_formats_100.py b/filters/intercept_some_message_formats_100.py index 5e236a59..6bb8091a 100644 --- a/filters/intercept_some_message_formats_100.py +++ b/filters/intercept_some_message_formats_100.py @@ -8,7 +8,7 @@ from filter import as_filter @as_filter(priority=100) def _filter(ctx_msg): msg_format = ctx_msg.get('format') - if msg_format != 'text' and ctx_msg.get('type') != 'friend_message': + if msg_format != 'text' and ctx_msg.get('msg_type') != 'private': return False if msg_format not in ('text', 'media'): return False diff --git a/filters/message_logger_1000.py b/filters/message_logger_1000.py index 642dfd26..15189b65 100644 --- a/filters/message_logger_1000.py +++ b/filters/message_logger_1000.py @@ -7,7 +7,10 @@ from filter import as_filter @as_filter(priority=1000) 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('discuss')) if ctx_msg.get('type') == 'discuss_message' else '') - + ': ' + ctx_msg.get('content')) + log = ctx_msg.get('sender') or ctx_msg.get('sender_id') or '未知用户' + if ctx_msg.get('msg_type') == 'group': + log += '@' + ctx_msg.get('group') or ctx_msg.get('group_id') or '未知群组' + if ctx_msg.get('msg_type') == 'discuss': + log += '@' + ctx_msg.get('discuss') or ctx_msg.get('discuss_id') or '未知讨论组' + log += ': ' + ctx_msg.get('content', '') + print(log) diff --git a/filters/split_at_xiaokai_50.py b/filters/split_at_xiaokai_50.py index d9a28b81..54fb55e8 100644 --- a/filters/split_at_xiaokai_50.py +++ b/filters/split_at_xiaokai_50.py @@ -3,12 +3,16 @@ This filter intercepts messages not intended to the bot and removes the beginnin """ from filter import as_filter -from apiclient import client as api +from msg_src_adapter import get_adapter_by_ctx @as_filter(priority=50) def _split_at_xiaokai(ctx_msg): - if ctx_msg.get('type') == 'group_message' or ctx_msg.get('type') == 'discuss_message': + if ctx_msg.get('is_at_me'): + # Directly return because it has been confirmed by previous processes + return True + + if ctx_msg.get('msg_type') == 'group' or ctx_msg.get('msg_type') == 'discuss': text = ctx_msg.get('text', '') if text.startswith('@'): my_group_nick = ctx_msg.get('receiver') @@ -16,10 +20,8 @@ def _split_at_xiaokai(ctx_msg): return False at_me = '@' + my_group_nick if not text.startswith(at_me): - user_info = api.get_user_info(ctx_msg).json() - if not user_info: - return False - my_nick = user_info.get('name') + user_info = get_adapter_by_ctx(ctx_msg).get_login_info(ctx_msg) + my_nick = user_info.get('nickname') if not my_nick: return False at_me = '@' + my_nick diff --git a/filters/unitize_context_message_10000.py b/filters/unitize_context_message_10000.py deleted file mode 100644 index 4936f3eb..00000000 --- a/filters/unitize_context_message_10000.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -This filter unitize context messages from different platform. -""" - -from filter import as_filter - - -@as_filter(priority=10000) -def _unitize(ctx_msg): - if 'group_uid' in ctx_msg: - ctx_msg['group_uid'] = str(ctx_msg['group_uid']) - if 'sender_uid' in ctx_msg: - ctx_msg['sender_uid'] = str(ctx_msg['sender_uid']) - if 'sender_id' in ctx_msg: - ctx_msg['sender_id'] = str(ctx_msg['sender_id']) - if 'discuss_id' in ctx_msg: - ctx_msg['discuss_id'] = str(ctx_msg['discuss_id']) - if 'group_id' in ctx_msg: - ctx_msg['group_id'] = str(ctx_msg['group_id']) - if 'id' in ctx_msg: - ctx_msg['id'] = str(ctx_msg['id']) - - if ctx_msg.get('via') == 'qq' and not ctx_msg.get('format'): - # All QQ messages that can be received are text - ctx_msg['format'] = 'text' diff --git a/little_shit.py b/little_shit.py index 102f53ab..b426806f 100644 --- a/little_shit.py +++ b/little_shit.py @@ -1,11 +1,8 @@ +import importlib import os -import hashlib -import random import functools -from datetime import datetime from config import config -from apiclient import client as api class SkipException(Exception): @@ -21,16 +18,21 @@ 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_plugin_dir(plugin_dir_name): + return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), plugin_dir_name)) -def get_commands_dir(): - return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), 'commands')) - - -def get_nl_processors_dir(): - return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), 'nl_processors')) +def load_plugins(plugin_dir_name, module_callback=None): + plugin_dir = get_plugin_dir(plugin_dir_name) + plugin_files = filter( + lambda filename: filename.endswith('.py') and not filename.startswith('_'), + os.listdir(plugin_dir) + ) + plugins = [os.path.splitext(file)[0] for file in plugin_files] + for mod_name in plugins: + mod = importlib.import_module(plugin_dir_name + '.' + mod_name) + if module_callback: + module_callback(mod) def get_db_dir(): @@ -46,47 +48,13 @@ def get_tmp_dir(): def get_source(ctx_msg): - """ - Source is used to distinguish the interactive sessions. - Note: This value may change after restarting the bot. - - :return: a 32 character unique string (md5) representing a source, or a random value if something strange happened - """ - source = None - if ctx_msg.get('via') == 'qq': - if ctx_msg.get('type') == 'group_message' and ctx_msg.get('group_uid') and ctx_msg.get('sender_uid'): - source = 'g' + ctx_msg.get('group_uid') + 'p' + ctx_msg.get('sender_uid') - elif ctx_msg.get('type') == 'discuss_message' and ctx_msg.get('discuss_id') and ctx_msg.get('sender_uid'): - source = 'd' + ctx_msg.get('discuss_id') + 'p' + ctx_msg.get('sender_uid') - elif ctx_msg.get('type') == 'friend_message' and ctx_msg.get('sender_uid'): - source = 'p' + ctx_msg.get('sender_uid') - elif ctx_msg.get('via') == 'wx': - if ctx_msg.get('type') == 'group_message' and ctx_msg.get('group_id') and ctx_msg.get('sender_id'): - source = 'g' + ctx_msg.get('group_id') + 'p' + ctx_msg.get('sender_id') - elif ctx_msg.get('type') == 'friend_message' and ctx_msg.get('sender_id'): - source = 'p' + ctx_msg.get('sender_id') - if not source: - source = str(int(datetime.now().timestamp())) + str(random.randint(100, 999)) - return hashlib.md5(source.encode('utf-8')).hexdigest() + from msg_src_adapter import get_adapter_by_ctx + return get_adapter_by_ctx(ctx_msg).get_source(ctx_msg) def get_target(ctx_msg): - """ - Target is used to distinguish the records in database. - Note: This value will not change after restarting the bot. - - :return: an unique string (account id with some flags) representing a target, - or None if there is no persistent unique value - """ - if ctx_msg.get('via') == 'qq': - if ctx_msg.get('type') == 'group_message' and ctx_msg.get('group_uid'): - return 'g' + ctx_msg.get('group_uid') - elif ctx_msg.get('type') == 'friend_message' and ctx_msg.get('sender_uid'): - return 'p' + ctx_msg.get('sender_uid') - elif ctx_msg.get('via') == 'wx': - if ctx_msg.get('type') == 'friend_message' and ctx_msg.get('sender_account'): - return 'p' + ctx_msg.get('sender_account') - return None + from msg_src_adapter import get_adapter_by_ctx + return get_adapter_by_ctx(ctx_msg).get_target(ctx_msg) def check_target(func): @@ -96,9 +64,11 @@ def check_target(func): @functools.wraps(func) def wrapper(args_text, ctx_msg, *args, **kwargs): - target = get_target(ctx_msg) + from msg_src_adapter import get_adapter_by_ctx + adapter = get_adapter_by_ctx(ctx_msg) + target = adapter.get_target(ctx_msg) if not target: - api.send_message('当前语境无法使用这个命令,请尝试发送私聊消息或稍后再试吧~', ctx_msg) + adapter.send_message(ctx_msg, '当前语境无法使用这个命令,请尝试发送私聊消息或稍后再试吧~') return else: return func(args_text, ctx_msg, *args, **kwargs) @@ -128,3 +98,7 @@ def get_fallback_command(): def get_fallback_command_after_nl_processors(): return config.get('fallback_command_after_nl_processors') + + +def get_message_sources(): + return config.get('message_sources', []) diff --git a/msg_src_adapter.py b/msg_src_adapter.py new file mode 100644 index 00000000..c1cf24bc --- /dev/null +++ b/msg_src_adapter.py @@ -0,0 +1,131 @@ +import hashlib +import random +from datetime import datetime + +from little_shit import get_message_sources + +_adapter_classes = {} # one message source, one adapter class +_adapter_instances = {} # one login account, one adapter instance + + +def as_adapter(via): + def decorator(cls): + _adapter_classes[via] = cls + return cls + + return decorator + + +class Adapter(object): + def __init__(self, config: dict): + self.login_id = config.get('login_id') + self.superuser_id = config.get('superuser_id') + + def unitize_context(self, ctx_msg: dict): + return None + + def send_message(self, target: dict, content): + if target is None or content is None: + return + + msg_type = target.get('msg_type', 'private') + if msg_type == 'group' and hasattr(self, 'send_group_message'): + self.send_group_message(target, content) + elif msg_type == 'discuss' and hasattr(self, 'send_discuss_message'): + self.send_discuss_message(target, content) + elif msg_type == 'private' and hasattr(self, 'send_private_message'): + if 'user_id' not in target and 'sender_id' in target: + target['user_id'] = target['sender_id'] # compatible with ctx_msg + elif 'user_tid' not in target and 'sender_tid' in target: + target['user_tid'] = target.get('sender_tid') # compatible with ctx_msg + + self.send_private_message(target, content) + + if 'user_id' in target and 'sender_id' in target: + del target['user_id'] + elif 'user_tid' in target and 'sender_tid' in target: + del target['user_tid'] + + def get_login_info(self, ctx_msg: dict): + return {'user_id': ctx_msg.get('login_id')} + + def is_sender_superuser(self, ctx_msg: dict): + return ctx_msg.get('sender_id') == self.superuser_id + + def get_sender_group_role(self, ctx_msg: dict): + return 'member' + + @staticmethod + def get_target(ctx_msg: dict): + """ + Target is used to distinguish the records in database. + Note: This value will not change after restarting the bot. + + :return: an unique string (account id with some flags) representing a target, + or None if there is no persistent unique value + """ + if ctx_msg.get('msg_type') == 'group' and ctx_msg.get('group_id'): + return 'g#' + ctx_msg.get('group_id') + elif ctx_msg.get('msg_type') == 'discuss' and ctx_msg.get('discuss_id'): + return 'd#' + ctx_msg.get('discuss_id') + elif ctx_msg.get('msg_type') == 'private' and ctx_msg.get('sender_id'): + return 'p#' + ctx_msg.get('sender_id') + return None + + @staticmethod + def get_source(ctx_msg: dict): + """ + Source is used to distinguish the interactive sessions. + Note: This value may change after restarting the bot. + + :return: a 32 character unique string (md5) representing a source, or a random value if something strange happened + """ + source = None + if ctx_msg.get('msg_type') == 'group' and ctx_msg.get('group_tid') and ctx_msg.get('sender_tid'): + source = 'g#' + ctx_msg.get('group_tid') + '#p#' + ctx_msg.get('sender_tid') + elif ctx_msg.get('msg_type') == 'discuss' and ctx_msg.get('discuss_tid') and ctx_msg.get('sender_tid'): + source = 'd#' + ctx_msg.get('discuss_tid') + '#p#' + ctx_msg.get('sender_tid') + elif ctx_msg.get('msg_type') == 'private' and ctx_msg.get('sender_tid'): + source = 'p#' + ctx_msg.get('sender_tid') + if not source: + source = str(int(datetime.now().timestamp())) + str(random.randint(100, 999)) + return hashlib.md5(source.encode('utf-8')).hexdigest() + + +class ConfigurationError(KeyError): + pass + + +def get_adapter_by_ctx(ctx_msg: dict): + if ctx_msg: + via = ctx_msg.get('via') + login_id = ctx_msg.get('login_id') + return get_adapter(via, login_id) + return None + + +def get_adapter(via: str, login_id: str): + if via == 'default': + # For the situations where 'via' does not matter, e.g. when we just want 'get_target' (which is universal) + if 'default' in _adapter_instances: + return _adapter_instances['default'] + else: + _adapter_instances['default'] = Adapter({}) + return _adapter_instances['default'] + + if not (via and login_id): + return None + + key = hashlib.md5(via.encode('utf-8') + login_id.encode('utf-8')).hexdigest() + if key in _adapter_instances: + return _adapter_instances[key] + else: + msg_src_list = list(filter( + lambda msg_src: msg_src['via'] == via and msg_src['login_id'] == login_id, + get_message_sources() + )) + if len(msg_src_list): + _adapter_instances[key] = _adapter_classes[via](msg_src_list[0]) + return _adapter_instances[key] + else: + return None diff --git a/msg_src_adapters/mojo_webqq.py b/msg_src_adapters/mojo_webqq.py new file mode 100644 index 00000000..f7cd920f --- /dev/null +++ b/msg_src_adapters/mojo_webqq.py @@ -0,0 +1,109 @@ +import requests + +from msg_src_adapter import Adapter, as_adapter, ConfigurationError + + +@as_adapter(via='mojo_webqq') +class MojoWebqqAdapter(Adapter): + def __init__(self, config: dict): + super().__init__(config) + if not config.get('api_url'): + raise ConfigurationError + self.api_url = config['api_url'] + + def unitize_context(self, ctx_msg: dict): + new_ctx = {'raw_ctx': ctx_msg, 'post_type': ctx_msg['post_type'], 'via': ctx_msg['via'], + 'login_id': ctx_msg['login_id']} + if new_ctx['post_type'] != 'receive_message': + return new_ctx + new_ctx['post_type'] = 'message' # Just handle 'receive_message', and make 'post_type' 'message' + new_ctx['time'] = ctx_msg['time'] + new_ctx['msg_id'] = str(ctx_msg['id']) + new_ctx['msg_type'] = ctx_msg['type'].split('_')[0] + new_ctx['msg_type'] = 'private' if new_ctx['msg_type'] == 'friend' else new_ctx['msg_type'] + new_ctx['format'] = 'text' + new_ctx['content'] = ctx_msg['content'] + + new_ctx['receiver'] = ctx_msg.get('receiver', '') + new_ctx['receiver_name'] = (requests.get(self.api_url + '/get_user_info').json() or {}).get('name', '') + new_ctx['receiver_id'] = str(ctx_msg.get('receiver_uid', '')) + new_ctx['receiver_tid'] = str(ctx_msg.get('receiver_id', '')) + + new_ctx['sender'] = ctx_msg.get('sender', '') + friend = list(filter( + lambda f: f.get('uid') == ctx_msg['sender_uid'], + requests.get(self.api_url + '/get_friend_info').json() or [] + )) + new_ctx['sender_name'] = friend[0].get('name', '') if friend else '' + new_ctx['sender_id'] = str(ctx_msg.get('sender_uid', '')) + new_ctx['sender_tid'] = str(ctx_msg.get('sender_id', '')) + + if new_ctx['msg_type'] == 'group': + new_ctx['group'] = ctx_msg.get('group', '') + new_ctx['group_id'] = str(ctx_msg.get('group_uid', '')) + new_ctx['group_tid'] = str(ctx_msg.get('group_id', '')) + + if new_ctx['msg_type'] == 'discuss': + new_ctx['discuss'] = ctx_msg.get('discuss', '') + new_ctx['discuss_tid'] = str(ctx_msg.get('discuss_id', '')) + + return new_ctx + + def get_login_info(self, ctx_msg: dict): + json = requests.get(self.api_url + '/get_user_info').json() + if json: + json['user_tid'] = json.get('id') + json['user_id'] = json.get('uid') + json['nickname'] = json.get('name') + return json + + def _get_group_info(self): + return requests.get(self.api_url + '/get_group_info').json() + + def get_sender_group_role(self, ctx_msg: dict): + groups = list(filter( + lambda g: str(g.get('id')) == ctx_msg['raw_ctx'].get('group_id'), + self._get_group_info() or [] + )) + if len(groups) <= 0 or 'member' not in groups[0]: + # This is strange, not likely happens + return 'member' + members = list(filter( + lambda m: str(m.get('id')) == ctx_msg['raw_ctx'].get('sender_id'), + groups[0].get('member') + )) + if len(members) <= 0: + # This is strange, not likely happens + return 'member' + return members[0].get('role', 'member') + + def send_private_message(self, target: dict, content: str): + params = None + if target.get('user_id'): + params = {'uid': target.get('user_id')} + elif target.get('user_tid'): + params = {'id': target.get('user_tid')} + + if params: + params['content'] = content + requests.get(self.api_url + '/send_friend_message', params=params) + + def send_group_message(self, target: dict, content: str): + params = None + if target.get('group_id'): + params = {'uid': target.get('group_id')} + elif target.get('group_tid'): + params = {'id': target.get('group_tid')} + + if params: + params['content'] = content + requests.get(self.api_url + '/send_group_message', params=params) + + def send_discuss_message(self, target: dict, content: str): + params = None + if target.get('discuss_tid'): + params = {'id': target.get('discuss_tid')} + + if params: + params['content'] = content + requests.get(self.api_url + '/send_discuss_message', params=params) diff --git a/nl_processor.py b/nl_processor.py index a5557718..4c358528 100644 --- a/nl_processor.py +++ b/nl_processor.py @@ -19,12 +19,13 @@ def as_processor(keywords=None): def parse_potential_commands(sentence): segmentation = list(jieba.posseg.cut(sentence=sentence)) - print('分词结果:', segmentation) + print('分词结果:', ['[' + s.flag + ']' + s.word for s in segmentation]) potential_commands = [] for processor in _processors: processed = False for regex in processor[0]: - for word, flag in segmentation: + for s in segmentation: + word, flag = s.word, s.flag if re.search(regex, word): result = processor[1](sentence, segmentation) if result: