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

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 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('/<string:via>/<string:login_id>', 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'))

View File

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

View File

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

View File

@ -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', '帮助', '用法', '使用帮助', '使用指南', '使用说明', '使用方法', '怎么用')

View File

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

View File

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

View File

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

View File

@ -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',
}
]
}

View File

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

View File

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

View File

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

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

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 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', [])

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