完成适配器化改造,所有上报数据统一、接口调用等都改成通过不同消息源的适配器来完成,插件和消息源的耦合

This commit is contained in:
Richard Chien 2017-02-15 15:52:18 +08:00
parent ecd446f057
commit 3508db348d
17 changed files with 383 additions and 221 deletions

37
app.py
View File

@ -1,31 +1,29 @@
import os import os
import importlib
from flask import Flask, request 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 filter import apply_filters
from msg_src_adapter import get_adapter
app = Flask(__name__) app = Flask(__name__)
@app.route('/qq/', methods=['POST']) @app.route('/<string:via>/<string:login_id>', methods=['POST'], strict_slashes=False)
def _handle_qq_message(): def _handle_via_account(via: str, login_id: str):
ctx_msg = request.json ctx_msg = request.json
ctx_msg['via'] = 'qq' ctx_msg['via'] = via
return _main(ctx_msg) ctx_msg['login_id'] = login_id
@app.route('/wx/', methods=['POST'])
def _handle_wx_message():
ctx_msg = request.json
ctx_msg['via'] = 'wx'
return _main(ctx_msg) return _main(ctx_msg)
def _main(ctx_msg: dict): def _main(ctx_msg: dict):
try: 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 raise SkipException
if not apply_filters(ctx_msg): if not apply_filters(ctx_msg):
raise SkipException raise SkipException
@ -36,16 +34,7 @@ def _main(ctx_msg: dict):
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)
if __name__ == '__main__': 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')) app.run(host=os.environ.get('HOST', '0.0.0.0'), port=os.environ.get('PORT', '8080'))

View File

@ -1,9 +1,8 @@
import functools import functools
import re import re
import os
from apiclient import client as api from little_shit import get_command_name_separators, get_command_args_separators
from little_shit import SkipException, 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_name_seps = get_command_name_separators()
_command_args_seps = get_command_args_separators() _command_args_seps = get_command_args_separators()
@ -117,13 +116,15 @@ class CommandRegistry:
if command_name in self.command_map: if command_name in self.command_map:
func = self.command_map[command_name] func = self.command_map[command_name]
if not self._check_scope(func, ctx_msg): if not self._check_scope(func, ctx_msg):
msg_type = ctx_msg.get('type') msg_type = ctx_msg.get('msg_type')
if msg_type == 'group_message': if msg_type == 'group':
msg_type_str = '群组消息' msg_type_str = '群组消息'
elif msg_type == 'discuss_message': elif msg_type == 'discuss':
msg_type_str = '讨论组消息' msg_type_str = '讨论组消息'
else: elif msg_type == 'private':
msg_type_str = '私聊消息' msg_type_str = '私聊消息'
else:
msg_type_str = '未知来源消息'
raise CommandScopeError(msg_type_str) raise CommandScopeError(msg_type_str)
if not self._check_permission(func, ctx_msg): if not self._check_permission(func, ctx_msg):
raise CommandPermissionError raise CommandPermissionError
@ -140,13 +141,13 @@ class CommandRegistry:
""" """
allowed_msg_type = set() allowed_msg_type = set()
if func.allow_group: if func.allow_group:
allowed_msg_type.add('group_message') allowed_msg_type.add('group')
if func.allow_discuss: if func.allow_discuss:
allowed_msg_type.add('discuss_message') allowed_msg_type.add('discuss')
if func.allow_private: 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 True
return False return False
@ -160,39 +161,23 @@ class CommandRegistry:
:param ctx_msg: context message :param ctx_msg: context message
:return: permitted or not :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 func.superuser_only:
if not b: return False
raise SkipException
try: if ctx_msg.get('msg_type') == 'group':
if func.superuser_only: # TODO: 在酷 Q 测试一下
raise SkipException allowed_roles = {'owner', 'admin', 'member'}
if ctx_msg.get('type') == 'group_message' and ctx_msg.get('via') == 'qq': if func.group_admin_only:
allowed_roles = {'owner', 'admin', 'member'} allowed_roles.intersection_update({'owner', 'admin'})
if func.group_admin_only: if func.group_owner_only:
allowed_roles.intersection_update({'owner', 'admin'}) allowed_roles.intersection_update({'owner'})
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
members = list(filter( role = adapter.get_sender_group_role(ctx_msg)
lambda m: str(m.get('id')) == ctx_msg.get('sender_id'), if role not in allowed_roles:
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'):
return False return False
# Still alive, let go # Still alive, let go

View File

@ -4,7 +4,6 @@ import requests
from command import CommandRegistry from command import CommandRegistry
from commands import core from commands import core
from little_shit import get_source from little_shit import get_source
from apiclient import client as api
__registry__ = cr = CommandRegistry() __registry__ = cr = CommandRegistry()
@ -33,15 +32,15 @@ def tuling123(args_text, ctx_msg, internal=False):
reply = '腊鸡图灵机器人出问题了,先不管他,过会儿再玩他' reply = '腊鸡图灵机器人出问题了,先不管他,过会儿再玩他'
core.echo(reply, ctx_msg) core.echo(reply, ctx_msg)
# TODO: 加入微信消息源之后修改
@cr.register('xiaoice', '小冰') # @cr.register('xiaoice', '小冰')
def xiaoice(args_text, ctx_msg, internal=False): # def xiaoice(args_text, ctx_msg, internal=False):
resp = api.wx_consult(account='xiaoice-ms', content=args_text) # resp = api.wx_consult(account='xiaoice-ms', content=args_text)
if resp: # if resp:
json = resp.json() # json = resp.json()
if json and json.get('reply'): # if json and json.get('reply'):
reply = json['reply'] # reply = json['reply']
core.echo(reply, ctx_msg, internal) # core.echo(reply, ctx_msg, internal)
return reply # return reply
core.echo('小冰没有回复,请稍后再试', ctx_msg, internal) # core.echo('小冰没有回复,请稍后再试', ctx_msg, internal)
return None # return None

View File

@ -1,5 +1,5 @@
from command import CommandRegistry from command import CommandRegistry
from apiclient import client as api from msg_src_adapter import get_adapter_by_ctx
__registry__ = cr = CommandRegistry() __registry__ = cr = CommandRegistry()
@ -9,7 +9,10 @@ def echo(args_text, ctx_msg, internal=False):
if internal: if internal:
return None return None
else: 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', '帮助', '用法', '使用帮助', '使用指南', '使用说明', '使用方法', '怎么用') @cr.register('help', '帮助', '用法', '使用帮助', '使用指南', '使用说明', '使用方法', '怎么用')

View File

@ -1,15 +1,12 @@
import os
import importlib
from command import CommandRegistry from command import CommandRegistry
from commands import core from commands import core
from nl_processor import parse_potential_commands 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 from command import hub as cmdhub
def _init(): def _init():
_load_processors() load_plugins('nl_processors')
__registry__ = cr = CommandRegistry(init_func=_init) __registry__ = cr = CommandRegistry(init_func=_init)
@ -41,13 +38,3 @@ def process(args_text, ctx_msg):
core.echo('暂时无法理解你的意思。\n' core.echo('暂时无法理解你的意思。\n'
'由于自然语言识别还非常不完善,建议使用命令来精确控制我。\n' '由于自然语言识别还非常不完善,建议使用命令来精确控制我。\n'
'如需帮助请发送「使用帮助」。', ctx_msg) '如需帮助请发送「使用帮助」。', 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)

View File

@ -1,6 +1,6 @@
import os import os
import re import re
from functools import reduce, wraps from functools import wraps
import pytz import pytz
import requests import requests

View File

@ -26,22 +26,21 @@ def test(_, ctx_msg):
@cr.register('block') @cr.register('block')
@cr.restrict(full_command_only=True, superuser_only=True) @cr.restrict(full_command_only=True, superuser_only=True)
@split_arguments(maxsplit=2) @split_arguments(maxsplit=1)
def block(_, ctx_msg, argv=None): def block(_, ctx_msg, argv=None):
def _send_error_msg(): def _send_error_msg():
core.echo('参数不正确。\n\n正确使用方法:\nsudo.block wx|qq <account-to-block>', ctx_msg) core.echo('参数不正确。\n\n正确使用方法:\nsudo.block <account-to-block>', ctx_msg)
if len(argv) != 2: if len(argv) != 1:
_send_error_msg() _send_error_msg()
return return
via, account = argv account = argv[0]
# Get a target using a fake context message # Get a target using a fake context message
target = get_target({ target = get_target({
'via': via, 'via': 'default',
'type': 'friend_message', 'msg_type': 'private',
'sender_uid': account, 'sender_id': account
'sender_account': account
}) })
if not target: if not target:
@ -65,31 +64,28 @@ def block_list(_, ctx_msg, internal=False):
if internal: if internal:
return blocked_targets return blocked_targets
if blocked_targets: if blocked_targets:
# `t[1:]` to reply user account, without target prefix 'p'. core.echo('已屏蔽的用户:\n' + ', '.join(blocked_targets), ctx_msg)
# This is a shit code, and should be changed later sometime.
core.echo('已屏蔽的用户:\n' + ', '.join([t[1:] for t in blocked_targets]), ctx_msg)
else: else:
core.echo('还没有屏蔽过用户', ctx_msg) core.echo('还没有屏蔽过用户', ctx_msg)
@cr.register('unblock') @cr.register('unblock')
@cr.restrict(full_command_only=True, superuser_only=True) @cr.restrict(full_command_only=True, superuser_only=True)
@split_arguments(maxsplit=2) @split_arguments(maxsplit=1)
def unblock(_, ctx_msg, argv=None): def unblock(_, ctx_msg, argv=None):
def _send_error_msg(): def _send_error_msg():
core.echo('参数不正确。\n\n正确使用方法:\nsudo.unblock wx|qq <account-to-unblock>', ctx_msg) core.echo('参数不正确。\n\n正确使用方法:\nsudo.unblock <account-to-unblock>', ctx_msg)
if len(argv) != 2: if len(argv) != 1:
_send_error_msg() _send_error_msg()
return return
via, account = argv account = argv[0]
# Get a target using a fake context message # Get a target using a fake context message
target = get_target({ target = get_target({
'via': via, 'via': 'default',
'type': 'friend_message', 'msg_type': 'private',
'sender_uid': account, 'sender_id': account
'sender_account': account
}) })
if not target: if not target:

View File

@ -5,4 +5,19 @@ config = {
'command_name_separators': ('->', '::', '/'), # Regex 'command_name_separators': ('->', '::', '/'), # Regex
'command_args_start_flags': ('', '', ',', ', ', ':', ': '), # Regex 'command_args_start_flags': ('', '', ',', ', ', ':', ': '), # Regex
'command_args_separators': ('', ','), # 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',
}
]
} }

View File

@ -1,6 +1,5 @@
import re import re
import sys import sys
import importlib
import interactive import interactive
from filter import as_filter 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() _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) @as_filter(priority=0)
def _dispatch_command(ctx_msg): def _dispatch_command(ctx_msg):
try: try:
@ -77,4 +62,12 @@ def _dispatch_command(ctx_msg):
core.echo('这个命令不支持' + se.msg_type + '哦~', 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)

View File

@ -8,7 +8,7 @@ from filter import as_filter
@as_filter(priority=100) @as_filter(priority=100)
def _filter(ctx_msg): def _filter(ctx_msg):
msg_format = ctx_msg.get('format') 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 return False
if msg_format not in ('text', 'media'): if msg_format not in ('text', 'media'):
return False return False

View File

@ -7,7 +7,10 @@ from filter import as_filter
@as_filter(priority=1000) @as_filter(priority=1000)
def _log_message(ctx_msg): def _log_message(ctx_msg):
print(ctx_msg.get('sender', '') log = ctx_msg.get('sender') or ctx_msg.get('sender_id') or '未知用户'
+ (('@' + ctx_msg.get('group')) if ctx_msg.get('type') == 'group_message' else '') if ctx_msg.get('msg_type') == 'group':
+ (('@' + ctx_msg.get('discuss')) if ctx_msg.get('type') == 'discuss_message' else '') log += '@' + ctx_msg.get('group') or ctx_msg.get('group_id') or '未知群组'
+ ': ' + ctx_msg.get('content')) 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)

View File

@ -3,12 +3,16 @@ This filter intercepts messages not intended to the bot and removes the beginnin
""" """
from filter import as_filter from filter import as_filter
from apiclient import client as api from msg_src_adapter import get_adapter_by_ctx
@as_filter(priority=50) @as_filter(priority=50)
def _split_at_xiaokai(ctx_msg): 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', '') text = ctx_msg.get('text', '')
if text.startswith('@'): if text.startswith('@'):
my_group_nick = ctx_msg.get('receiver') my_group_nick = ctx_msg.get('receiver')
@ -16,10 +20,8 @@ def _split_at_xiaokai(ctx_msg):
return False return False
at_me = '@' + my_group_nick at_me = '@' + my_group_nick
if not text.startswith(at_me): if not text.startswith(at_me):
user_info = api.get_user_info(ctx_msg).json() user_info = get_adapter_by_ctx(ctx_msg).get_login_info(ctx_msg)
if not user_info: my_nick = user_info.get('nickname')
return False
my_nick = user_info.get('name')
if not my_nick: if not my_nick:
return False return False
at_me = '@' + my_nick at_me = '@' + my_nick

View File

@ -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'

View File

@ -1,11 +1,8 @@
import importlib
import os import os
import hashlib
import random
import functools import functools
from datetime import datetime
from config import config from config import config
from apiclient import client as api
class SkipException(Exception): class SkipException(Exception):
@ -21,16 +18,21 @@ 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(): def get_plugin_dir(plugin_dir_name):
return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), 'filters')) return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), plugin_dir_name))
def get_commands_dir(): def load_plugins(plugin_dir_name, module_callback=None):
return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), 'commands')) plugin_dir = get_plugin_dir(plugin_dir_name)
plugin_files = filter(
lambda filename: filename.endswith('.py') and not filename.startswith('_'),
def get_nl_processors_dir(): os.listdir(plugin_dir)
return _mkdir_if_not_exists_and_return_path(os.path.join(get_root_dir(), 'nl_processors')) )
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(): def get_db_dir():
@ -46,47 +48,13 @@ def get_tmp_dir():
def get_source(ctx_msg): def get_source(ctx_msg):
""" from msg_src_adapter import get_adapter_by_ctx
Source is used to distinguish the interactive sessions. return get_adapter_by_ctx(ctx_msg).get_source(ctx_msg)
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()
def get_target(ctx_msg): def get_target(ctx_msg):
""" from msg_src_adapter import get_adapter_by_ctx
Target is used to distinguish the records in database. return get_adapter_by_ctx(ctx_msg).get_target(ctx_msg)
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
def check_target(func): def check_target(func):
@ -96,9 +64,11 @@ def check_target(func):
@functools.wraps(func) @functools.wraps(func)
def wrapper(args_text, ctx_msg, *args, **kwargs): 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: if not target:
api.send_message('当前语境无法使用这个命令,请尝试发送私聊消息或稍后再试吧~', ctx_msg) adapter.send_message(ctx_msg, '当前语境无法使用这个命令,请尝试发送私聊消息或稍后再试吧~')
return return
else: else:
return func(args_text, ctx_msg, *args, **kwargs) return func(args_text, ctx_msg, *args, **kwargs)
@ -128,3 +98,7 @@ def get_fallback_command():
def get_fallback_command_after_nl_processors(): def get_fallback_command_after_nl_processors():
return config.get('fallback_command_after_nl_processors') return config.get('fallback_command_after_nl_processors')
def get_message_sources():
return config.get('message_sources', [])

131
msg_src_adapter.py Normal file
View File

@ -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

View File

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

View File

@ -19,12 +19,13 @@ def as_processor(keywords=None):
def parse_potential_commands(sentence): def parse_potential_commands(sentence):
segmentation = list(jieba.posseg.cut(sentence=sentence)) segmentation = list(jieba.posseg.cut(sentence=sentence))
print('分词结果:', segmentation) print('分词结果:', ['[' + s.flag + ']' + s.word for s in segmentation])
potential_commands = [] potential_commands = []
for processor in _processors: for processor in _processors:
processed = False processed = False
for regex in processor[0]: 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): if re.search(regex, word):
result = processor[1](sentence, segmentation) result = processor[1](sentence, segmentation)
if result: if result: