From a13128f3566a1230392be18092e5ad61c6064e6e Mon Sep 17 00:00:00 2001 From: Richard Chien Date: Sun, 1 Jul 2018 11:01:24 +0800 Subject: [PATCH] Lots of updates --- none/__init__.py | 2 +- none/{command/__init__.py => command.py} | 70 +++++------------- none/command/permissions.py | 25 ------- none/message.py | 11 +-- none/natural_language.py | 22 ++++++ none/permission.py | 91 ++++++++++++++++++++++++ none/plugins/base.py | 7 +- none_demo/plugins/weather/__init__.py | 22 +++++- setup.py | 2 +- 9 files changed, 162 insertions(+), 90 deletions(-) rename none/{command/__init__.py => command.py} (81%) delete mode 100644 none/command/permissions.py create mode 100644 none/natural_language.py create mode 100644 none/permission.py diff --git a/none/__init__.py b/none/__init__.py index 057be8e6..9bef65d7 100644 --- a/none/__init__.py +++ b/none/__init__.py @@ -74,7 +74,7 @@ def load_builtin_plugins(): load_plugins(plugin_dir, 'none.plugins') -from .command import on_command, CommandSession +from .command import on_command, CommandSession, CommandGroup from .notice_request import ( on_notice, NoticeSession, on_request, RequestSession, diff --git a/none/command/__init__.py b/none/command.py similarity index 81% rename from none/command/__init__.py rename to none/command.py index 294871a3..80e0c979 100644 --- a/none/command/__init__.py +++ b/none/command.py @@ -5,13 +5,13 @@ from typing import ( Tuple, Union, Callable, Iterable, Dict, Any, Optional, Sequence ) -from aiocqhttp import CQHttp, Error as CQHttpError +from aiocqhttp import CQHttp from aiocqhttp.message import Message -from . import permissions as perm -from ..helpers import context_source -from ..expression import render -from ..session import BaseSession +from . import permission as perm +from .helpers import context_source +from .expression import render +from .session import BaseSession # Key: str (one segment of command name) # Value: subtree or a leaf Command object @@ -37,17 +37,20 @@ class Command: self.only_to_me = only_to_me self.args_parser_func = None - async def run(self, session, *, permission: Optional[int] = None) -> bool: + async def run(self, session, check_perm: bool = True) -> bool: """ Run the command in a given session. :param session: CommandSession object - :param permission: the permission the caller owns + :param check_perm: should check permission before running :return: the command is finished """ - if permission is None: - permission = await _calculate_permission(session.bot, session.ctx) - if self.func and permission & self.permission: + if check_perm: + has_perm = await perm.check_permission( + session.bot, session.ctx, self.permission) + else: + has_perm = True + if self.func and has_perm: if self.args_parser_func: await self.args_parser_func(session) await self.func(session) @@ -55,48 +58,6 @@ class Command: return False -async def _calculate_permission(bot: CQHttp, ctx: Dict[str, Any]) -> int: - """ - Calculate the permission OWNED by the current context. - - This is different from the permission REQUIRED by a command. - The result of this function should be made a bit-and with - the permission required by some command to check whether - the context is allowed to call the command. - - :param bot: CQHttp instance - :param ctx: message context - :return: the calculated permission value - """ - permission = 0 - if ctx['user_id'] in bot.config.SUPERUSERS: - permission |= perm.IS_SUPERUSER - if ctx['message_type'] == 'private': - if ctx['sub_type'] == 'friend': - permission |= perm.IS_PRIVATE_FRIEND - elif ctx['sub_type'] == 'group': - permission |= perm.IS_PRIVATE_GROUP - elif ctx['sub_type'] == 'discuss': - permission |= perm.IS_PRIVATE_DISCUSS - elif ctx['sub_type'] == 'other': - permission |= perm.IS_PRIVATE_OTHER - elif ctx['message_type'] == 'group': - permission |= perm.IS_GROUP_MEMBER - if not ctx['anonymous']: - try: - member_info = await bot.get_group_member_info(**ctx) - if member_info: - if member_info['role'] == 'owner': - permission |= perm.IS_GROUP_OWNER - elif member_info['role'] == 'admin': - permission |= perm.IS_GROUP_ADMIN - except CQHttpError: - pass - elif ctx['message_type'] == 'discuss': - permission |= perm.IS_DISCUSS - return permission - - def on_command(name: Union[str, Tuple[str]], *, aliases: Iterable = (), permission: int = perm.EVERYBODY, @@ -325,10 +286,13 @@ async def handle_command(bot: CQHttp, ctx: Dict[str, Any]) -> bool: """ src = context_source(ctx) session = None + check_perm = True if _sessions.get(src): session = _sessions[src] if session and session.is_valid: session.refresh(ctx, current_arg=str(ctx['message'])) + # there is no need to check permission for existing session + check_perm = False else: # the session is expired, remove it del _sessions[src] @@ -340,7 +304,7 @@ async def handle_command(bot: CQHttp, ctx: Dict[str, Any]) -> bool: _sessions[src] = session try: - res = await session.cmd.run(session) + res = await session.cmd.run(session, check_perm=check_perm) # the command is finished, remove the session del _sessions[src] return res diff --git a/none/command/permissions.py b/none/command/permissions.py deleted file mode 100644 index 9915063e..00000000 --- a/none/command/permissions.py +++ /dev/null @@ -1,25 +0,0 @@ -PRIVATE_FRIEND = 0x0001 -PRIVATE_GROUP = 0x0002 -PRIVATE_DISCUSS = 0x0004 -PRIVATE_OTHER = 0x0008 -PRIVATE = 0x000F -DISCUSS = 0x00F0 -GROUP_MEMBER = 0x0100 -GROUP_ADMIN = 0x0200 -GROUP_OWNER = 0x0400 -GROUP = 0x0F00 -SUPERUSER = 0xF000 -EVERYBODY = 0xFFFF - -IS_NOBODY = 0x0000 -IS_PRIVATE_FRIEND = PRIVATE_FRIEND -IS_PRIVATE_GROUP = PRIVATE_GROUP -IS_PRIVATE_DISCUSS = PRIVATE_DISCUSS -IS_PRIVATE_OTHER = PRIVATE_OTHER -IS_PRIVATE = PRIVATE -IS_DISCUSS = DISCUSS -IS_GROUP_MEMBER = GROUP_MEMBER -IS_GROUP_ADMIN = GROUP_MEMBER | GROUP_ADMIN -IS_GROUP_OWNER = GROUP_ADMIN | GROUP_OWNER -IS_GROUP = GROUP -IS_SUPERUSER = 0xFFFF diff --git a/none/message.py b/none/message.py index 010da6d1..57b68bd1 100644 --- a/none/message.py +++ b/none/message.py @@ -4,8 +4,8 @@ from aiocqhttp import CQHttp from aiocqhttp.message import MessageSegment from .command import handle_command +from .natural_language import handle_natural_language from .log import logger -from .helpers import send async def handle_message(bot: CQHttp, ctx: Dict[str, Any]) -> None: @@ -23,9 +23,10 @@ async def handle_message(bot: CQHttp, ctx: Dict[str, Any]) -> None: handled = await handle_command(bot, ctx) if handled: - logger.debug('Message is handled as a command') + logger.debug('Message is handled as command') return - elif ctx['to_me']: - await send(bot, ctx, '你在说什么我看不懂诶') - # TODO: NLP + handled = await handle_natural_language(bot, ctx) + if handled: + logger.debug('Message is handled as natural language') + return diff --git a/none/natural_language.py b/none/natural_language.py new file mode 100644 index 00000000..dc1f68be --- /dev/null +++ b/none/natural_language.py @@ -0,0 +1,22 @@ +from collections import namedtuple +from typing import Dict, Any + +from aiocqhttp import CQHttp + +_nl_processors = set() + + +class NLProcessor: + __slots__ = ('func', 'permission', 'only_to_me', 'keywords', + 'precondition_func') + + +NLPResult = namedtuple('NLPResult', ( + 'confidence', + 'cmd_name', + 'cmd_args', +)) + + +async def handle_natural_language(bot: CQHttp, ctx: Dict[str, Any]) -> None: + pass diff --git a/none/permission.py b/none/permission.py new file mode 100644 index 00000000..b08a52c7 --- /dev/null +++ b/none/permission.py @@ -0,0 +1,91 @@ +from collections import namedtuple +from typing import Dict, Any + +from aiocache import cached +from aiocqhttp import CQHttp, Error as CQHttpError + +PRIVATE_FRIEND = 0x0001 +PRIVATE_GROUP = 0x0002 +PRIVATE_DISCUSS = 0x0004 +PRIVATE_OTHER = 0x0008 +PRIVATE = 0x000F +DISCUSS = 0x00F0 +GROUP_MEMBER = 0x0100 +GROUP_ADMIN = 0x0200 +GROUP_OWNER = 0x0400 +GROUP = 0x0F00 +SUPERUSER = 0xF000 +EVERYBODY = 0xFFFF + +IS_NOBODY = 0x0000 +IS_PRIVATE_FRIEND = PRIVATE_FRIEND +IS_PRIVATE_GROUP = PRIVATE_GROUP +IS_PRIVATE_DISCUSS = PRIVATE_DISCUSS +IS_PRIVATE_OTHER = PRIVATE_OTHER +IS_PRIVATE = PRIVATE +IS_DISCUSS = DISCUSS +IS_GROUP_MEMBER = GROUP_MEMBER +IS_GROUP_ADMIN = GROUP_MEMBER | GROUP_ADMIN +IS_GROUP_OWNER = GROUP_ADMIN | GROUP_OWNER +IS_GROUP = GROUP +IS_SUPERUSER = 0xFFFF + +_min_context_fields = ( + 'message_type', + 'sub_type', + 'user_id', + 'discuss_id', + 'group_id', + 'anonymous', +) + +_MinContext = namedtuple('MinContext', _min_context_fields) + + +async def check_permission(bot: CQHttp, ctx: Dict[str, Any], + permission_required: int) -> bool: + min_ctx_kwargs = {} + for field in _min_context_fields: + if field in ctx: + min_ctx_kwargs[field] = ctx[field] + else: + min_ctx_kwargs[field] = None + min_ctx = _MinContext(**min_ctx_kwargs) + return await _check(bot, min_ctx, permission_required) + + +@cached(ttl=2 * 60) # cache the result for 1 minute +async def _check(bot: CQHttp, min_ctx: _MinContext, + permission_required: int) -> bool: + permission = 0 + if min_ctx.user_id in bot.config.SUPERUSERS: + permission |= IS_SUPERUSER + if min_ctx.message_type == 'private': + if min_ctx.sub_type == 'friend': + permission |= IS_PRIVATE_FRIEND + elif min_ctx.sub_type == 'group': + permission |= IS_PRIVATE_GROUP + elif min_ctx.sub_type == 'discuss': + permission |= IS_PRIVATE_DISCUSS + elif min_ctx.sub_type == 'other': + permission |= IS_PRIVATE_OTHER + elif min_ctx.message_type == 'group': + permission |= IS_GROUP_MEMBER + if not min_ctx.anonymous: + try: + member_info = await bot.get_group_member_info( + group_id=min_ctx.group_id, + user_id=min_ctx.user_id, + no_cache=True + ) + if member_info: + if member_info['role'] == 'owner': + permission |= IS_GROUP_OWNER + elif member_info['role'] == 'admin': + permission |= IS_GROUP_ADMIN + except CQHttpError: + pass + elif min_ctx.message_type == 'discuss': + permission |= IS_DISCUSS + + return bool(permission & permission_required) diff --git a/none/plugins/base.py b/none/plugins/base.py index bc47eea2..c6648a27 100644 --- a/none/plugins/base.py +++ b/none/plugins/base.py @@ -1,15 +1,14 @@ from aiocqhttp.message import unescape -from none import on_command, CommandSession -from none.command import permissions as perm +from none import on_command, CommandSession, permission as perm @on_command('echo', only_to_me=False) async def echo(session: CommandSession): - await session.send(session.args.get('text') or session.current_arg) + await session.send(session.args.get('message') or session.current_arg) @on_command('say', permission=perm.SUPERUSER, only_to_me=False) async def _(session: CommandSession): await session.send( - unescape(session.args.get('text') or session.current_arg)) + unescape(session.args.get('message') or session.current_arg)) diff --git a/none_demo/plugins/weather/__init__.py b/none_demo/plugins/weather/__init__.py index dc51b79a..0b112e18 100644 --- a/none_demo/plugins/weather/__init__.py +++ b/none_demo/plugins/weather/__init__.py @@ -1,4 +1,4 @@ -from none.command import CommandSession, CommandGroup +from none import CommandSession, CommandGroup from . import expressions as expr @@ -17,6 +17,26 @@ async def _(session: CommandSession): session.args[session.current_key] = session.current_arg_text.strip() +# @on_natural_language(keywords={'天气', '雨', '雪', '晴', '阴', '多云', '冰雹'}, +# only_to_me=False) +# async def weather_nlp(session: NaturalLanguageSession): +# return NLPResult(89.5, ('weather', 'weather'), {'city': '南京'}) +# +# +# @weather_nlp.condition +# async def _(session: NaturalLanguageSession): +# keywords = {'天气', '雨', '雪', '晴', '阴', '多云', '冰雹'} +# for kw in keywords: +# if kw in session.text: +# keyword_hit = True +# break +# else: +# keyword_hit = False +# if session.ctx['to_me'] and keyword_hit: +# return True +# return False + + @w.command('suggestion', aliases=('生活指数', '生活建议', '生活提示')) async def suggestion(session: CommandSession): await session.send('suggestion') diff --git a/setup.py b/setup.py index 626fccaa..42f37faf 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( description='A QQ bot framework', long_description=long_description, long_description_content_type="text/markdown", - install_requires=['aiocqhttp>=0.3'], + install_requires=['aiocqhttp>=0.3', 'aiocache'], python_requires='>=3.6', platforms='any', classifiers=(