Add CommandSession.switch()

This commit is contained in:
Richard Chien 2018-07-24 23:59:45 +08:00
parent fa32ad6425
commit de9a4e5b83
2 changed files with 63 additions and 6 deletions

View File

@ -170,7 +170,7 @@ class _FurtherInteractionNeeded(Exception):
class _FinishException(Exception): class _FinishException(Exception):
""" """
Raised by session.finish() indicating that the command session Raised by session.finish() indicating that the command session
should be stop and removed. should be stopped and removed.
""" """
def __init__(self, result: bool = True): def __init__(self, result: bool = True):
@ -180,6 +180,24 @@ class _FinishException(Exception):
self.result = result self.result = result
class SwitchException(Exception):
"""
Raised by session.switch() indicating that the command session
should be stopped and replaced with a new one (going through
handle_message() again).
Since the new context message will go through handle_message()
again, the later function should be notified. So this exception
is designed to be propagated to handle_message().
"""
def __init__(self, new_ctx_message: Message):
"""
:param new_ctx_message: new message which should be placed in context
"""
self.new_ctx_message = new_ctx_message
class CommandSession(BaseSession): class CommandSession(BaseSession):
__slots__ = ('cmd', 'current_key', 'current_arg', 'current_arg_text', __slots__ = ('cmd', 'current_key', 'current_arg', 'current_arg_text',
'current_arg_images', 'args', '_last_interaction', '_running') 'current_arg_images', 'args', '_last_interaction', '_running')
@ -278,6 +296,26 @@ class CommandSession(BaseSession):
asyncio.ensure_future(self.send(message)) asyncio.ensure_future(self.send(message))
raise _FinishException raise _FinishException
# noinspection PyMethodMayBeStatic
def switch(self, new_ctx_message: Any) -> None:
"""
Finish the session and switch to a new (fake) message context.
The user may send another command (or another intention as natural
language) when interacting with the current session. In this case,
the session may not understand what the user is saying, so it
should call this method and pass in that message, then NoneBot will
handle the situation properly.
"""
if self.is_first_run:
# if calling this method during first run,
# we think the command is not handled
raise _FinishException(result=False)
if not isinstance(new_ctx_message, Message):
new_ctx_message = Message(new_ctx_message)
raise SwitchException(new_ctx_message)
def parse_command(bot: NoneBot, def parse_command(bot: NoneBot,
cmd_string: str) -> Tuple[Optional[Command], Optional[str]]: cmd_string: str) -> Tuple[Optional[Command], Optional[str]]:
@ -433,9 +471,11 @@ async def _real_run_command(session: CommandSession,
ctx_id: str, ctx_id: str,
disable_interaction: bool = False, disable_interaction: bool = False,
**kwargs) -> bool: **kwargs) -> bool:
session_overridden = False
if not disable_interaction: if not disable_interaction:
# override session only when not disabling interaction # override session only when not disabling interaction
_sessions[ctx_id] = session _sessions[ctx_id] = session
session_overridden = True
try: try:
logger.debug(f'Running command {session.cmd.name}') logger.debug(f'Running command {session.cmd.name}')
session.running = True session.running = True
@ -450,10 +490,20 @@ async def _real_run_command(session: CommandSession,
f'command {session.cmd.name}') f'command {session.cmd.name}')
# return True because this step of the session is successful # return True because this step of the session is successful
return True return True
except _FinishException as e: except (_FinishException, SwitchException) as e:
session.running = False session.running = False
logger.debug(f'Session of command {session.cmd.name} finished') logger.debug(f'Session of command {session.cmd.name} finished')
if not disable_interaction and ctx_id in _sessions: if session_overridden:
# the command is finished, remove the session # the command is finished, remove the session
del _sessions[ctx_id] del _sessions[ctx_id]
return e.result
if isinstance(e, _FinishException):
return e.result
elif isinstance(e, SwitchException):
# we are guaranteed that the session is not first run here
if ctx_id in _sessions:
# make sure there is no session waiting
del _sessions[ctx_id]
logger.debug(f'Session of command {session.cmd.name} switching,'
f'new context message: {e.new_ctx_message}')
raise e # this is intended to be propagated to handle_message()

View File

@ -3,7 +3,7 @@ from typing import Dict, Any
from aiocqhttp.message import MessageSegment from aiocqhttp.message import MessageSegment
from . import NoneBot from . import NoneBot
from .command import handle_command from .command import handle_command, SwitchException
from .log import logger from .log import logger
from .natural_language import handle_natural_language from .natural_language import handle_natural_language
@ -23,7 +23,14 @@ async def handle_message(bot: NoneBot, ctx: Dict[str, Any]) -> None:
else: else:
ctx['to_me'] = True ctx['to_me'] = True
handled = await handle_command(bot, ctx) while True:
try:
handled = await handle_command(bot, ctx)
break
except SwitchException as e:
# we are sure that there is no session existing now
ctx['message'] = e.new_ctx_message
ctx['to_me'] = True
if handled: if handled:
logger.info(f'Message {ctx["message_id"]} is handled as a command') logger.info(f'Message {ctx["message_id"]} is handled as a command')
return return