rename session.ctx to session.event for consistency

This commit is contained in:
Richard Chien 2020-03-15 22:48:22 +08:00
parent c366e6c950
commit 9b54af70e6
9 changed files with 216 additions and 200 deletions

View File

@ -2,7 +2,7 @@ import asyncio
import logging import logging
from typing import Any, Optional from typing import Any, Optional
import aiocqhttp.message import aiocqhttp
from aiocqhttp import CQHttp from aiocqhttp import CQHttp
from .log import logger from .log import logger
@ -32,16 +32,16 @@ class NoneBot(CQHttp):
from .notice_request import handle_notice_or_request from .notice_request import handle_notice_or_request
@self.on_message @self.on_message
async def _(ctx): async def _(event: aiocqhttp.Event):
asyncio.ensure_future(handle_message(self, ctx)) asyncio.create_task(handle_message(self, event))
@self.on_notice @self.on_notice
async def _(ctx): async def _(event: aiocqhttp.Event):
asyncio.ensure_future(handle_notice_or_request(self, ctx)) asyncio.create_task(handle_notice_or_request(self, event))
@self.on_request @self.on_request
async def _(ctx): async def _(event: aiocqhttp.Event):
asyncio.ensure_future(handle_notice_or_request(self, ctx)) asyncio.create_task(handle_notice_or_request(self, event))
def run(self, host: Optional[str] = None, port: Optional[int] = None, def run(self, host: Optional[str] = None, port: Optional[int] = None,
*args, **kwargs) -> None: *args, **kwargs) -> None:

View File

@ -9,6 +9,8 @@ from typing import (
Awaitable Awaitable
) )
from aiocqhttp import Event as CQEvent
from nonebot import NoneBot, permission as perm from nonebot import NoneBot, permission as perm
from nonebot.command.argfilter import ValidateError from nonebot.command.argfilter import ValidateError
from nonebot.helpers import context_id, send, render_expression from nonebot.helpers import context_id, send, render_expression
@ -16,8 +18,7 @@ from nonebot.log import logger
from nonebot.message import Message from nonebot.message import Message
from nonebot.session import BaseSession from nonebot.session import BaseSession
from nonebot.typing import ( from nonebot.typing import (
Context_T, CommandName_T, CommandArgs_T, Message_T, State_T, CommandName_T, CommandArgs_T, Message_T, State_T, Filter_T
Filter_T
) )
# key: one segment of command name # key: one segment of command name
@ -129,7 +130,7 @@ class Command:
:param session: CommandSession object :param session: CommandSession object
:return: the session has the permission :return: the session has the permission
""" """
return await perm.check_permission(session.bot, session.ctx, return await perm.check_permission(session.bot, session.event,
self.permission) self.permission)
def __repr__(self): def __repr__(self):
@ -166,6 +167,17 @@ def on_command(name: Union[str, CommandName_T], *,
cmd = Command(name=cmd_name, func=func, permission=permission, cmd = Command(name=cmd_name, func=func, permission=permission,
only_to_me=only_to_me, privileged=privileged) only_to_me=only_to_me, privileged=privileged)
def args_parser(parser_func: CommandHandler_T) -> CommandHandler_T:
"""
Decorator to register a function as the arguments parser of
the corresponding command.
"""
cmd.args_parser_func = parser_func
return parser_func
func.args_parser = args_parser
if shell_like: if shell_like:
async def shell_like_args_parser(session): async def shell_like_args_parser(session):
session.args['argv'] = shlex.split(session.current_arg) session.args['argv'] = shlex.split(session.current_arg)
@ -190,15 +202,6 @@ def on_command(name: Union[str, CommandName_T], *,
for alias in aliases: for alias in aliases:
_aliases[alias] = cmd_name _aliases[alias] = cmd_name
def args_parser(parser_func: CommandHandler_T) -> CommandHandler_T:
"""
Decorator to register a function as the arguments parser of
the corresponding command.
"""
cmd.args_parser_func = parser_func
return parser_func
func.args_parser = args_parser
return func return func
return deco return deco
@ -246,16 +249,16 @@ class SwitchException(Exception):
should be stopped and replaced with a new one (going through should be stopped and replaced with a new one (going through
handle_message() again). handle_message() again).
Since the new context message will go through handle_message() Since the new message will go through handle_message() again,
again, the later function should be notified. So this exception the later function should be notified. So this exception is
is designed to be propagated to handle_message(). intended to be propagated to handle_message().
""" """
def __init__(self, new_ctx_message: Message): def __init__(self, new_message: Message):
""" """
:param new_ctx_message: new message which should be placed in context :param new_message: new message which should be placed in event
""" """
self.new_ctx_message = new_ctx_message self.new_message = new_message
class CommandSession(BaseSession): class CommandSession(BaseSession):
@ -264,9 +267,9 @@ class CommandSession(BaseSession):
'current_arg', '_current_arg_text', '_current_arg_images', 'current_arg', '_current_arg_text', '_current_arg_images',
'_state', '_last_interaction', '_running', '_run_future') '_state', '_last_interaction', '_running', '_run_future')
def __init__(self, bot: NoneBot, ctx: Context_T, cmd: Command, *, def __init__(self, bot: NoneBot, event: CQEvent, cmd: Command, *,
current_arg: str = '', args: Optional[CommandArgs_T] = None): current_arg: str = '', args: Optional[CommandArgs_T] = None):
super().__init__(bot, ctx) super().__init__(bot, event)
self.cmd = cmd # Command object self.cmd = cmd # Command object
# unique key of the argument that is currently requesting (asking) # unique key of the argument that is currently requesting (asking)
@ -281,7 +284,7 @@ class CommandSession(BaseSession):
self.current_arg: str = '' # with potential CQ codes self.current_arg: str = '' # with potential CQ codes
self._current_arg_text = None self._current_arg_text = None
self._current_arg_images = None self._current_arg_images = None
self.refresh(ctx, current_arg=current_arg) # fill the above self.refresh(event, current_arg=current_arg) # fill the above
self._run_future = partial(asyncio.run_coroutine_threadsafe, self._run_future = partial(asyncio.run_coroutine_threadsafe,
loop=bot.loop) loop=bot.loop)
@ -363,14 +366,14 @@ class CommandSession(BaseSession):
""" """
return self.state.get('argv', []) return self.state.get('argv', [])
def refresh(self, ctx: Context_T, *, current_arg: str = '') -> None: def refresh(self, event: CQEvent, *, current_arg: str = '') -> None:
""" """
Refill the session with a new message context. Refill the session with a new message event.
:param ctx: new message context :param event: new message event
:param current_arg: new command argument as a string :param current_arg: new command argument as a string
""" """
self.ctx = ctx self.event = event
self.current_arg = current_arg self.current_arg = current_arg
self._current_arg_text = None self._current_arg_text = None
self._current_arg_images = None self._current_arg_images = None
@ -421,9 +424,9 @@ class CommandSession(BaseSession):
self._run_future(self.send(message, **kwargs)) self._run_future(self.send(message, **kwargs))
raise _FinishException raise _FinishException
def switch(self, new_ctx_message: Message_T) -> None: def switch(self, new_message: Message_T) -> None:
""" """
Finish the session and switch to a new (fake) message context. Finish the session and switch to a new (fake) message event.
The user may send another command (or another intention as natural The user may send another command (or another intention as natural
language) when interacting with the current session. In this case, language) when interacting with the current session. In this case,
@ -436,9 +439,9 @@ class CommandSession(BaseSession):
# we think the command is not handled # we think the command is not handled
raise _FinishException(result=False) raise _FinishException(result=False)
if not isinstance(new_ctx_message, Message): if not isinstance(new_message, Message):
new_ctx_message = Message(new_ctx_message) new_message = Message(new_message)
raise SwitchException(new_ctx_message) raise SwitchException(new_message)
def parse_command(bot: NoneBot, def parse_command(bot: NoneBot,
@ -513,26 +516,26 @@ def parse_command(bot: NoneBot,
return cmd, ''.join(cmd_remained) return cmd, ''.join(cmd_remained)
async def handle_command(bot: NoneBot, ctx: Context_T) -> bool: async def handle_command(bot: NoneBot, event: CQEvent) -> bool:
""" """
Handle a message as a command. Handle a message as a command.
This function is typically called by "handle_message". This function is typically called by "handle_message".
:param bot: NoneBot instance :param bot: NoneBot instance
:param ctx: message context :param event: message event
:return: the message is handled as a command :return: the message is handled as a command
""" """
cmd, current_arg = parse_command(bot, str(ctx['message']).lstrip()) cmd, current_arg = parse_command(bot, str(event.message).lstrip())
is_privileged_cmd = cmd and cmd.privileged is_privileged_cmd = cmd and cmd.privileged
if is_privileged_cmd and cmd.only_to_me and not ctx['to_me']: if is_privileged_cmd and cmd.only_to_me and not event['to_me']:
is_privileged_cmd = False is_privileged_cmd = False
disable_interaction = is_privileged_cmd disable_interaction = is_privileged_cmd
if is_privileged_cmd: if is_privileged_cmd:
logger.debug(f'Command {cmd.name} is a privileged command') logger.debug(f'Command {cmd.name} is a privileged command')
ctx_id = context_id(ctx) ctx_id = context_id(event)
if not is_privileged_cmd: if not is_privileged_cmd:
# wait for 1.5 seconds (at most) if the current session is running # wait for 1.5 seconds (at most) if the current session is running
@ -549,7 +552,7 @@ async def handle_command(bot: NoneBot, ctx: Context_T) -> bool:
logger.warning(f'There is a session of command ' logger.warning(f'There is a session of command '
f'{session.cmd.name} running, notify the user') f'{session.cmd.name} running, notify the user')
asyncio.ensure_future(send( asyncio.ensure_future(send(
bot, ctx, bot, event,
render_expression(bot.config.SESSION_RUNNING_EXPRESSION) render_expression(bot.config.SESSION_RUNNING_EXPRESSION)
)) ))
# pretend we are successful, so that NLP won't handle it # pretend we are successful, so that NLP won't handle it
@ -558,8 +561,8 @@ async def handle_command(bot: NoneBot, ctx: Context_T) -> bool:
if session.is_valid: if session.is_valid:
logger.debug(f'Session of command {session.cmd.name} exists') logger.debug(f'Session of command {session.cmd.name} exists')
# since it's in a session, the user must be talking to me # since it's in a session, the user must be talking to me
ctx['to_me'] = True event['to_me'] = True
session.refresh(ctx, current_arg=str(ctx['message'])) session.refresh(event, current_arg=str(event['message']))
# there is no need to check permission for existing session # there is no need to check permission for existing session
check_perm = False check_perm = False
else: else:
@ -573,17 +576,17 @@ async def handle_command(bot: NoneBot, ctx: Context_T) -> bool:
if not cmd: if not cmd:
logger.debug('Not a known command, ignored') logger.debug('Not a known command, ignored')
return False return False
if cmd.only_to_me and not ctx['to_me']: if cmd.only_to_me and not event['to_me']:
logger.debug('Not to me, ignored') logger.debug('Not to me, ignored')
return False return False
session = CommandSession(bot, ctx, cmd, current_arg=current_arg) session = CommandSession(bot, event, cmd, current_arg=current_arg)
logger.debug(f'New session of command {session.cmd.name} created') logger.debug(f'New session of command {session.cmd.name} created')
return await _real_run_command(session, ctx_id, check_perm=check_perm, return await _real_run_command(session, ctx_id, check_perm=check_perm,
disable_interaction=disable_interaction) disable_interaction=disable_interaction)
async def call_command(bot: NoneBot, ctx: Context_T, async def call_command(bot: NoneBot, event: CQEvent,
name: Union[str, CommandName_T], *, name: Union[str, CommandName_T], *,
current_arg: str = '', current_arg: str = '',
args: Optional[CommandArgs_T] = None, args: Optional[CommandArgs_T] = None,
@ -601,7 +604,7 @@ async def call_command(bot: NoneBot, ctx: Context_T,
the user for more info). the user for more info).
:param bot: NoneBot instance :param bot: NoneBot instance
:param ctx: message context :param event: message event
:param name: command name :param name: command name
:param current_arg: command current argument string :param current_arg: command current argument string
:param args: command args :param args: command args
@ -612,10 +615,13 @@ async def call_command(bot: NoneBot, ctx: Context_T,
cmd = _find_command(name) cmd = _find_command(name)
if not cmd: if not cmd:
return False return False
session = CommandSession(bot, ctx, cmd, current_arg=current_arg, args=args) session = CommandSession(bot, event, cmd,
return await _real_run_command(session, context_id(session.ctx), current_arg=current_arg, args=args)
return await _real_run_command(
session, context_id(session.event),
check_perm=check_perm, check_perm=check_perm,
disable_interaction=disable_interaction) disable_interaction=disable_interaction
)
async def _real_run_command(session: CommandSession, async def _real_run_command(session: CommandSession,
@ -674,18 +680,18 @@ async def _real_run_command(session: CommandSession,
# make sure there is no session waiting # make sure there is no session waiting
del _sessions[ctx_id] del _sessions[ctx_id]
logger.debug(f'Session of command {session.cmd.name} switching, ' logger.debug(f'Session of command {session.cmd.name} switching, '
f'new context message: {e.new_ctx_message}') f'new message: {e.new_message}')
raise e # this is intended to be propagated to handle_message() raise e # this is intended to be propagated to handle_message()
def kill_current_session(ctx: Context_T) -> None: def kill_current_session(event: CQEvent) -> None:
""" """
Force kill current session of the given context, Force kill current session of the given event context,
despite whether it is running or not. despite whether it is running or not.
:param ctx: message context :param event: message event
""" """
ctx_id = context_id(ctx) ctx_id = context_id(event)
if ctx_id in _sessions: if ctx_id in _sessions:
del _sessions[ctx_id] del _sessions[ctx_id]

View File

@ -2,51 +2,53 @@ import hashlib
import random import random
from typing import Sequence, Callable, Any from typing import Sequence, Callable, Any
from aiocqhttp import Event as CQEvent
from . import NoneBot from . import NoneBot
from .exceptions import CQHttpError from .exceptions import CQHttpError
from .message import escape from .message import escape
from .typing import Context_T, Message_T, Expression_T from .typing import Message_T, Expression_T
def context_id(ctx: Context_T, *, def context_id(event: CQEvent, *,
mode: str = 'default', use_hash: bool = False) -> str: mode: str = 'default', use_hash: bool = False) -> str:
""" """
Calculate a unique id representing the current context. Calculate a unique id representing the context of the given event.
mode: mode:
default: one id for one context default: one id for one context
group: one id for one group or discuss group: one id for one group or discuss
user: one id for one user user: one id for one user
:param ctx: the context dict :param event: the event object
:param mode: unique id mode: "default", "group", or "user" :param mode: unique id mode: "default", "group", or "user"
:param use_hash: use md5 to hash the id or not :param use_hash: use md5 to hash the id or not
""" """
ctx_id = '' ctx_id = ''
if mode == 'default': if mode == 'default':
if ctx.get('group_id'): if event.group_id:
ctx_id = f'/group/{ctx["group_id"]}' ctx_id = f'/group/{event.group_id}'
elif ctx.get('discuss_id'): elif event.discuss_id:
ctx_id = f'/discuss/{ctx["discuss_id"]}' ctx_id = f'/discuss/{event.discuss_id}'
if ctx.get('user_id'): if event.user_id:
ctx_id += f'/user/{ctx["user_id"]}' ctx_id += f'/user/{event.user_id}'
elif mode == 'group': elif mode == 'group':
if ctx.get('group_id'): if event.group_id:
ctx_id = f'/group/{ctx["group_id"]}' ctx_id = f'/group/{event.group_id}'
elif ctx.get('discuss_id'): elif event.discuss_id:
ctx_id = f'/discuss/{ctx["discuss_id"]}' ctx_id = f'/discuss/{event.discuss_id}'
elif ctx.get('user_id'): elif event.user_id:
ctx_id = f'/user/{ctx["user_id"]}' ctx_id = f'/user/{event.user_id}'
elif mode == 'user': elif mode == 'user':
if ctx.get('user_id'): if event.user_id:
ctx_id = f'/user/{ctx["user_id"]}' ctx_id = f'/user/{event.user_id}'
if ctx_id and use_hash: if ctx_id and use_hash:
ctx_id = hashlib.md5(ctx_id.encode('ascii')).hexdigest() ctx_id = hashlib.md5(ctx_id.encode('ascii')).hexdigest()
return ctx_id return ctx_id
async def send(bot: NoneBot, ctx: Context_T, async def send(bot: NoneBot, event: CQEvent,
message: Message_T, *, message: Message_T, *,
ensure_private: bool = False, ensure_private: bool = False,
ignore_failure: bool = True, ignore_failure: bool = True,
@ -54,9 +56,9 @@ async def send(bot: NoneBot, ctx: Context_T,
"""Send a message ignoring failure by default.""" """Send a message ignoring failure by default."""
try: try:
if ensure_private: if ensure_private:
ctx = ctx.copy() event = event.copy()
ctx['message_type'] = 'private' event['message_type'] = 'private'
return await bot.send(ctx, message, **kwargs) return await bot.send(event, message, **kwargs)
except CQHttpError: except CQHttpError:
if not ignore_failure: if not ignore_failure:
raise raise

View File

@ -1,13 +1,13 @@
import asyncio import asyncio
from typing import Callable from typing import Callable
from aiocqhttp import Event as CQEvent
from aiocqhttp.message import * from aiocqhttp.message import *
from . import NoneBot from . import NoneBot
from .command import handle_command, SwitchException 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
from .typing import Context_T
_message_preprocessors = set() _message_preprocessors = set()
@ -17,76 +17,77 @@ def message_preprocessor(func: Callable) -> Callable:
return func return func
async def handle_message(bot: NoneBot, ctx: Context_T) -> None: async def handle_message(bot: NoneBot, event: CQEvent) -> None:
_log_message(ctx) _log_message(event)
if not ctx['message']: assert isinstance(event.message, Message)
ctx['message'].append(MessageSegment.text('')) if not event.message:
event.message.append(MessageSegment.text(''))
coros = [] coros = []
for processor in _message_preprocessors: for preprocessor in _message_preprocessors:
coros.append(processor(bot, ctx)) coros.append(preprocessor(bot, event))
if coros: if coros:
await asyncio.wait(coros) await asyncio.wait(coros)
raw_to_me = ctx.get('to_me', False) raw_to_me = event.get('to_me', False)
_check_at_me(bot, ctx) _check_at_me(bot, event)
_check_calling_me_nickname(bot, ctx) _check_calling_me_nickname(bot, event)
ctx['to_me'] = raw_to_me or ctx['to_me'] event['to_me'] = raw_to_me or event['to_me']
while True: while True:
try: try:
handled = await handle_command(bot, ctx) handled = await handle_command(bot, event)
break break
except SwitchException as e: except SwitchException as e:
# we are sure that there is no session existing now # we are sure that there is no session existing now
ctx['message'] = e.new_ctx_message event['message'] = e.new_message
ctx['to_me'] = True event['to_me'] = True
if handled: if handled:
logger.info(f'Message {ctx["message_id"]} is handled as a command') logger.info(f'Message {event.message_id} is handled as a command')
return return
handled = await handle_natural_language(bot, ctx) handled = await handle_natural_language(bot, event)
if handled: if handled:
logger.info(f'Message {ctx["message_id"]} is handled ' logger.info(f'Message {event.message_id} is handled '
f'as natural language') f'as natural language')
return return
def _check_at_me(bot: NoneBot, ctx: Context_T) -> None: def _check_at_me(bot: NoneBot, event: CQEvent) -> None:
if ctx['message_type'] == 'private': if event.detail_type == 'private':
ctx['to_me'] = True event['to_me'] = True
else: else:
# group or discuss # group or discuss
ctx['to_me'] = False event['to_me'] = False
at_me_seg = MessageSegment.at(ctx['self_id']) at_me_seg = MessageSegment.at(event.self_id)
# check the first segment # check the first segment
first_msg_seg = ctx['message'][0] first_msg_seg = event.message[0]
if first_msg_seg == at_me_seg: if first_msg_seg == at_me_seg:
ctx['to_me'] = True event['to_me'] = True
del ctx['message'][0] del event.message[0]
if not ctx['to_me']: if not event['to_me']:
# check the last segment # check the last segment
i = -1 i = -1
last_msg_seg = ctx['message'][i] last_msg_seg = event.message[i]
if last_msg_seg.type == 'text' and \ if last_msg_seg.type == 'text' and \
not last_msg_seg.data['text'].strip() and \ not last_msg_seg.data['text'].strip() and \
len(ctx['message']) >= 2: len(event.message) >= 2:
i -= 1 i -= 1
last_msg_seg = ctx['message'][i] last_msg_seg = event.message[i]
if last_msg_seg == at_me_seg: if last_msg_seg == at_me_seg:
ctx['to_me'] = True event['to_me'] = True
del ctx['message'][i:] del event.message[i:]
if not ctx['message']: if not event.message:
ctx['message'].append(MessageSegment.text('')) event.message.append(MessageSegment.text(''))
def _check_calling_me_nickname(bot: NoneBot, ctx: Context_T) -> None: def _check_calling_me_nickname(bot: NoneBot, event: CQEvent) -> None:
first_msg_seg = ctx['message'][0] first_msg_seg = event.message[0]
if first_msg_seg.type != 'text': if first_msg_seg.type != 'text':
return return
@ -105,16 +106,16 @@ def _check_calling_me_nickname(bot: NoneBot, ctx: Context_T) -> None:
if m: if m:
nickname = m.group(1) nickname = m.group(1)
logger.debug(f'User is calling me {nickname}') logger.debug(f'User is calling me {nickname}')
ctx['to_me'] = True event['to_me'] = True
first_msg_seg.data['text'] = first_text[m.end():] first_msg_seg.data['text'] = first_text[m.end():]
def _log_message(ctx: Context_T) -> None: def _log_message(event: CQEvent) -> None:
msg_from = str(ctx['user_id']) msg_from = str(event.user_id)
if ctx['message_type'] == 'group': if event.detail_type == 'group':
msg_from += f'@[群:{ctx["group_id"]}]' msg_from += f'@[群:{event.group_id}]'
elif ctx['message_type'] == 'discuss': elif event.detail_type == 'discuss':
msg_from += f'@[讨论组:{ctx["discuss_id"]}]' msg_from += f'@[讨论组:{event.discuss_id}]'
logger.info(f'Self: {ctx["self_id"]}, ' logger.info(f'Self: {event.self_id}, '
f'Message {ctx["message_id"]} from {msg_from}: ' f'Message {event.message_id} from {msg_from}: '
f'{str(ctx["message"]).__repr__()}') f'{str(event.message).__repr__()}')

View File

@ -1,12 +1,14 @@
import asyncio import asyncio
from typing import Iterable, Optional, Callable, Union, NamedTuple from typing import Iterable, Optional, Callable, Union, NamedTuple
from aiocqhttp import Event as CQEvent
from . import NoneBot, permission as perm from . import NoneBot, permission as perm
from .command import call_command from .command import call_command
from .log import logger from .log import logger
from .message import Message from .message import Message
from .session import BaseSession from .session import BaseSession
from .typing import Context_T, CommandName_T, CommandArgs_T from .typing import CommandName_T, CommandArgs_T
_nl_processors = set() _nl_processors = set()
@ -27,8 +29,9 @@ class NLProcessor:
self.allow_empty_message = allow_empty_message self.allow_empty_message = allow_empty_message
def on_natural_language(keywords: Union[Optional[Iterable], str, Callable] = None, def on_natural_language(
*, permission: int = perm.EVERYBODY, keywords: Union[Optional[Iterable], str, Callable] = None, *,
permission: int = perm.EVERYBODY,
only_to_me: bool = True, only_to_me: bool = True,
only_short_message: bool = True, only_short_message: bool = True,
allow_empty_message: bool = False) -> Callable: allow_empty_message: bool = False) -> Callable:
@ -63,8 +66,8 @@ def on_natural_language(keywords: Union[Optional[Iterable], str, Callable] = Non
class NLPSession(BaseSession): class NLPSession(BaseSession):
__slots__ = ('msg', 'msg_text', 'msg_images') __slots__ = ('msg', 'msg_text', 'msg_images')
def __init__(self, bot: NoneBot, ctx: Context_T, msg: str): def __init__(self, bot: NoneBot, event: CQEvent, msg: str):
super().__init__(bot, ctx) super().__init__(bot, event)
self.msg = msg self.msg = msg
tmp_msg = Message(msg) tmp_msg = Message(msg)
self.msg_text = tmp_msg.extract_plain_text() self.msg_text = tmp_msg.extract_plain_text()
@ -97,17 +100,17 @@ class IntentCommand(NamedTuple):
current_arg: str = '' current_arg: str = ''
async def handle_natural_language(bot: NoneBot, ctx: Context_T) -> bool: async def handle_natural_language(bot: NoneBot, event: CQEvent) -> bool:
""" """
Handle a message as natural language. Handle a message as natural language.
This function is typically called by "handle_message". This function is typically called by "handle_message".
:param bot: NoneBot instance :param bot: NoneBot instance
:param ctx: message context :param event: message event
:return: the message is handled as natural language :return: the message is handled as natural language
""" """
session = NLPSession(bot, ctx, str(ctx['message'])) session = NLPSession(bot, event, str(event.message))
# use msg_text here because CQ code "share" may be very long, # use msg_text here because CQ code "share" may be very long,
# at the same time some plugins may want to handle it # at the same time some plugins may want to handle it
@ -123,10 +126,10 @@ async def handle_natural_language(bot: NoneBot, ctx: Context_T) -> bool:
msg_text_length > bot.config.SHORT_MESSAGE_MAX_LENGTH: msg_text_length > bot.config.SHORT_MESSAGE_MAX_LENGTH:
continue continue
if p.only_to_me and not ctx['to_me']: if p.only_to_me and not event['to_me']:
continue continue
should_run = await perm.check_permission(bot, ctx, p.permission) should_run = await perm.check_permission(bot, event, p.permission)
if should_run and p.keywords: if should_run and p.keywords:
for kw in p.keywords: for kw in p.keywords:
if kw in session.msg_text: if kw in session.msg_text:
@ -162,7 +165,7 @@ async def handle_natural_language(bot: NoneBot, ctx: Context_T) -> bool:
logger.debug( logger.debug(
f'Intent command with highest confidence: {chosen_cmd}') f'Intent command with highest confidence: {chosen_cmd}')
return await call_command( return await call_command(
bot, ctx, chosen_cmd.name, bot, event, chosen_cmd.name,
args=chosen_cmd.args, args=chosen_cmd.args,
current_arg=chosen_cmd.current_arg, current_arg=chosen_cmd.current_arg,
check_perm=False check_perm=False

View File

@ -1,12 +1,12 @@
from typing import Optional, Callable, Union from typing import Optional, Callable, Union
from aiocqhttp import Event as CQEvent
from aiocqhttp.bus import EventBus from aiocqhttp.bus import EventBus
from . import NoneBot from . import NoneBot
from .exceptions import CQHttpError from .exceptions import CQHttpError
from .log import logger from .log import logger
from .session import BaseSession from .session import BaseSession
from .typing import Context_T
_bus = EventBus() _bus = EventBus()
@ -36,15 +36,15 @@ on_request = _make_event_deco('request')
class NoticeSession(BaseSession): class NoticeSession(BaseSession):
__slots__ = () __slots__ = ()
def __init__(self, bot: NoneBot, ctx: Context_T): def __init__(self, bot: NoneBot, event: CQEvent):
super().__init__(bot, ctx) super().__init__(bot, event)
class RequestSession(BaseSession): class RequestSession(BaseSession):
__slots__ = () __slots__ = ()
def __init__(self, bot: NoneBot, ctx: Context_T): def __init__(self, bot: NoneBot, event: CQEvent):
super().__init__(bot, ctx) super().__init__(bot, event)
async def approve(self, remark: str = '') -> None: async def approve(self, remark: str = '') -> None:
""" """
@ -55,8 +55,8 @@ class RequestSession(BaseSession):
try: try:
await self.bot.call_action( await self.bot.call_action(
action='.handle_quick_operation_async', action='.handle_quick_operation_async',
self_id=self.ctx.get('self_id'), self_id=self.event.self_id,
context=self.ctx, context=self.event,
operation={'approve': True, 'remark': remark} operation={'approve': True, 'remark': remark}
) )
except CQHttpError: except CQHttpError:
@ -71,39 +71,34 @@ class RequestSession(BaseSession):
try: try:
await self.bot.call_action( await self.bot.call_action(
action='.handle_quick_operation_async', action='.handle_quick_operation_async',
self_id=self.ctx.get('self_id'), self_id=self.event.self_id,
context=self.ctx, context=self.event,
operation={'approve': False, 'reason': reason} operation={'approve': False, 'reason': reason}
) )
except CQHttpError: except CQHttpError:
pass pass
async def handle_notice_or_request(bot: NoneBot, ctx: Context_T) -> None: async def handle_notice_or_request(bot: NoneBot, event: CQEvent) -> None:
post_type = ctx['post_type'] # "notice" or "request" if event.type == 'notice':
detail_type = ctx[f'{post_type}_type'] _log_notice(event)
event = f'{post_type}.{detail_type}' session = NoticeSession(bot, event)
if ctx.get('sub_type'):
event += f'.{ctx["sub_type"]}'
if post_type == 'notice':
_log_notice(ctx)
session = NoticeSession(bot, ctx)
else: # must be 'request' else: # must be 'request'
_log_request(ctx) _log_request(event)
session = RequestSession(bot, ctx) session = RequestSession(bot, event)
logger.debug(f'Emitting event: {event}') ev_name = event.name
logger.debug(f'Emitting event: {ev_name}')
try: try:
await _bus.emit(event, session) await _bus.emit(ev_name, session)
except Exception as e: except Exception as e:
logger.error(f'An exception occurred while handling event {event}:') logger.error(f'An exception occurred while handling event {ev_name}:')
logger.exception(e) logger.exception(e)
def _log_notice(ctx: Context_T) -> None: def _log_notice(event: CQEvent) -> None:
logger.info(f'Notice: {ctx}') logger.info(f'Notice: {event}')
def _log_request(ctx: Context_T) -> None: def _log_request(event: CQEvent) -> None:
logger.info(f'Request: {ctx}') logger.info(f'Request: {event}')

View File

@ -1,10 +1,10 @@
from collections import namedtuple from collections import namedtuple
from aiocache import cached from aiocache import cached
from aiocqhttp import Event as CQEvent
from . import NoneBot from . import NoneBot
from .exceptions import CQHttpError from .exceptions import CQHttpError
from .typing import Context_T
PRIVATE_FRIEND = 0x0001 PRIVATE_FRIEND = 0x0001
PRIVATE_GROUP = 0x0002 PRIVATE_GROUP = 0x0002
@ -32,7 +32,7 @@ IS_GROUP_OWNER = GROUP_ADMIN | GROUP_OWNER
IS_GROUP = GROUP IS_GROUP = GROUP
IS_SUPERUSER = 0xFFFF IS_SUPERUSER = 0xFFFF
_min_context_fields = ( _min_event_fields = (
'self_id', 'self_id',
'message_type', 'message_type',
'sub_type', 'sub_type',
@ -42,52 +42,52 @@ _min_context_fields = (
'anonymous', 'anonymous',
) )
_MinContext = namedtuple('MinContext', _min_context_fields) _MinEvent = namedtuple('MinEvent', _min_event_fields)
async def check_permission(bot: NoneBot, ctx: Context_T, async def check_permission(bot: NoneBot, event: CQEvent,
permission_required: int) -> bool: permission_required: int) -> bool:
""" """
Check if the context has the permission required. Check if the event context has the permission required.
:param bot: NoneBot instance :param bot: NoneBot instance
:param ctx: message context :param event: message event
:param permission_required: permission required :param permission_required: permission required
:return: the context has the permission :return: the context has the permission
""" """
min_ctx_kwargs = {} min_event_kwargs = {}
for field in _min_context_fields: for field in _min_event_fields:
if field in ctx: if field in event:
min_ctx_kwargs[field] = ctx[field] min_event_kwargs[field] = event[field]
else: else:
min_ctx_kwargs[field] = None min_event_kwargs[field] = None
min_ctx = _MinContext(**min_ctx_kwargs) min_event = _MinEvent(**min_event_kwargs)
return await _check(bot, min_ctx, permission_required) return await _check(bot, min_event, permission_required)
@cached(ttl=2 * 60) # cache the result for 2 minute @cached(ttl=2 * 60) # cache the result for 2 minute
async def _check(bot: NoneBot, min_ctx: _MinContext, async def _check(bot: NoneBot, min_event: _MinEvent,
permission_required: int) -> bool: permission_required: int) -> bool:
permission = 0 permission = 0
if min_ctx.user_id in bot.config.SUPERUSERS: if min_event.user_id in bot.config.SUPERUSERS:
permission |= IS_SUPERUSER permission |= IS_SUPERUSER
if min_ctx.message_type == 'private': if min_event.message_type == 'private':
if min_ctx.sub_type == 'friend': if min_event.sub_type == 'friend':
permission |= IS_PRIVATE_FRIEND permission |= IS_PRIVATE_FRIEND
elif min_ctx.sub_type == 'group': elif min_event.sub_type == 'group':
permission |= IS_PRIVATE_GROUP permission |= IS_PRIVATE_GROUP
elif min_ctx.sub_type == 'discuss': elif min_event.sub_type == 'discuss':
permission |= IS_PRIVATE_DISCUSS permission |= IS_PRIVATE_DISCUSS
elif min_ctx.sub_type == 'other': elif min_event.sub_type == 'other':
permission |= IS_PRIVATE_OTHER permission |= IS_PRIVATE_OTHER
elif min_ctx.message_type == 'group': elif min_event.message_type == 'group':
permission |= IS_GROUP_MEMBER permission |= IS_GROUP_MEMBER
if not min_ctx.anonymous: if not min_event.anonymous:
try: try:
member_info = await bot.get_group_member_info( member_info = await bot.get_group_member_info(
self_id=min_ctx.self_id, self_id=min_event.self_id,
group_id=min_ctx.group_id, group_id=min_event.group_id,
user_id=min_ctx.user_id, user_id=min_event.user_id,
no_cache=True no_cache=True
) )
if member_info: if member_info:
@ -97,7 +97,7 @@ async def _check(bot: NoneBot, min_ctx: _MinContext,
permission |= IS_GROUP_ADMIN permission |= IS_GROUP_ADMIN
except CQHttpError: except CQHttpError:
pass pass
elif min_ctx.message_type == 'discuss': elif min_event.message_type == 'discuss':
permission |= IS_DISCUSS permission |= IS_DISCUSS
return bool(permission & permission_required) return bool(permission & permission_required)

View File

@ -1,18 +1,28 @@
from aiocqhttp import Event as CQEvent
from . import NoneBot from . import NoneBot
from .helpers import send from .helpers import send
from .typing import Context_T, Message_T from .typing import Message_T
class BaseSession: class BaseSession:
__slots__ = ('bot', 'ctx') __slots__ = ('bot', 'event')
def __init__(self, bot: NoneBot, ctx: Context_T): def __init__(self, bot: NoneBot, event: CQEvent):
self.bot = bot self.bot = bot
self.ctx = ctx self.event = event
@property
def ctx(self) -> CQEvent:
return self.event
@ctx.setter
def ctx(self, val: CQEvent) -> None:
self.event = val
@property @property
def self_id(self) -> int: def self_id(self) -> int:
return self.ctx['self_id'] return self.event.self_id
async def send(self, message: Message_T, *, async def send(self, message: Message_T, *,
at_sender: bool = False, at_sender: bool = False,
@ -28,7 +38,7 @@ class BaseSession:
:param ignore_failure: if any CQHttpError raised, ignore it :param ignore_failure: if any CQHttpError raised, ignore it
:return: the result returned by CQHTTP :return: the result returned by CQHTTP
""" """
return await send(self.bot, self.ctx, message, return await send(self.bot, self.event, message,
at_sender=at_sender, at_sender=at_sender,
ensure_private=ensure_private, ensure_private=ensure_private,
ignore_failure=ignore_failure, **kwargs) ignore_failure=ignore_failure, **kwargs)

View File

@ -1,6 +1,5 @@
from typing import Union, List, Dict, Any, Sequence, Callable, Tuple, Awaitable from typing import Union, List, Dict, Any, Sequence, Callable, Tuple, Awaitable
Context_T = Dict[str, Any]
Message_T = Union[str, Dict[str, Any], List[Dict[str, Any]]] Message_T = Union[str, Dict[str, Any], List[Dict[str, Any]]]
Expression_T = Union[str, Sequence[str], Callable] Expression_T = Union[str, Sequence[str], Callable]
CommandName_T = Tuple[str, ...] CommandName_T = Tuple[str, ...]