From 761d725aede02ca325e5ba29858f487e696b6728 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Tue, 5 May 2020 16:11:05 +0800 Subject: [PATCH] change Matcher class --- nonebot/event.py | 124 +++++++++++++++++++++++++++ nonebot/exception.py | 9 +- nonebot/matcher.py | 197 ++++++++++++++++++++++++++++--------------- nonebot/rule.py | 8 +- 4 files changed, 265 insertions(+), 73 deletions(-) create mode 100644 nonebot/event.py diff --git a/nonebot/event.py b/nonebot/event.py new file mode 100644 index 00000000..f79d0396 --- /dev/null +++ b/nonebot/event.py @@ -0,0 +1,124 @@ +from typing import Dict, Any, Optional + + +class Event(dict): + """ + 封装从 CQHTTP 收到的事件数据对象(字典),提供属性以获取其中的字段。 + + 除 `type` 和 `detail_type` 属性对于任何事件都有效外,其它属性存在与否(不存在则返回 + `None`)依事件不同而不同。 + """ + + @staticmethod + def from_payload(payload: Dict[str, Any]) -> Optional["Event"]: + """ + 从 CQHTTP 事件数据构造 `Event` 对象。 + """ + try: + e = Event(payload) + _ = e.type, e.detail_type + return e + except KeyError: + return None + + @property + def type(self) -> str: + """ + 事件类型,有 ``message``、``notice``、``request``、``meta_event`` 等。 + """ + return self['post_type'] + + @property + def detail_type(self) -> str: + """ + 事件具体类型,依 `type` 的不同而不同,以 ``message`` 类型为例,有 + ``private``、``group``、``discuss`` 等。 + """ + return self[f'{self.type}_type'] + + @property + def sub_type(self) -> Optional[str]: + """ + 事件子类型,依 `detail_type` 不同而不同,以 ``message.private`` 为例,有 + ``friend``、``group``、``discuss``、``other`` 等。 + """ + return self.get('sub_type') + + @property + def name(self): + """ + 事件名,对于有 `sub_type` 的事件,为 ``{type}.{detail_type}.{sub_type}``,否则为 + ``{type}.{detail_type}``。 + """ + n = self.type + '.' + self.detail_type + if self.sub_type: + n += '.' + self.sub_type + return n + + @property + def self_id(self) -> int: + """机器人自身 ID。""" + return self['self_id'] + + @property + def user_id(self) -> Optional[int]: + """用户 ID。""" + return self.get('user_id') + + @property + def operator_id(self) -> Optional[int]: + """操作者 ID。""" + return self.get('operator_id') + + @property + def group_id(self) -> Optional[int]: + """群 ID。""" + return self.get('group_id') + + @property + def discuss_id(self) -> Optional[int]: + """讨论组 ID。""" + return self.get('discuss_id') + + @property + def message_id(self) -> Optional[int]: + """消息 ID。""" + return self.get('message_id') + + @property + def message(self) -> Optional[Any]: + """消息。""" + return self.get('message') + + @property + def raw_message(self) -> Optional[str]: + """未经 CQHTTP 处理的原始消息。""" + return self.get('raw_message') + + @property + def sender(self) -> Optional[Dict[str, Any]]: + """消息发送者信息。""" + return self.get('sender') + + @property + def anonymous(self) -> Optional[Dict[str, Any]]: + """匿名信息。""" + return self.get('anonymous') + + @property + def file(self) -> Optional[Dict[str, Any]]: + """文件信息。""" + return self.get('file') + + @property + def comment(self) -> Optional[str]: + """请求验证消息。""" + return self.get('comment') + + @property + def flag(self) -> Optional[str]: + """请求标识。""" + return self.get('flag') + + def __repr__(self) -> str: + return f'' diff --git a/nonebot/exception.py b/nonebot/exception.py index aa4342c0..de674b15 100644 --- a/nonebot/exception.py +++ b/nonebot/exception.py @@ -1,8 +1,13 @@ -class BlockedException(Exception): - """Block a message from further handling""" +class PausedException(Exception): + """Block a message from further handling and try to receive a new message""" pass class RejectedException(Exception): """Reject a message and return current handler back""" pass + + +class FinishedException(Exception): + """Finish handling a message""" + pass diff --git a/nonebot/matcher.py b/nonebot/matcher.py index ed003259..dada2f59 100644 --- a/nonebot/matcher.py +++ b/nonebot/matcher.py @@ -1,102 +1,163 @@ import re import copy from functools import wraps -from typing import Union, Optional +from typing import Type, Union, Optional, Callable -from .rule import Rule, startswith, regex, user +from .event import Event from .typing import Scope, Handler -from .exception import BlockedException, RejectedException +from .rule import Rule, startswith, regex, user +from .exception import PausedException, RejectedException, FinishedException class Matcher: - def __init__(self, - rule: Rule, - scope: Scope = "ALL", - permission: str = "ALL", - block: bool = True, - *, - handlers: list = [], - state: dict = {}, - temp: bool = False): - self.rule = rule - self.scope = scope - self.permission = permission - self.block = block - self.handlers = handlers - self.state = state - self.temp = temp + rule: Rule = Rule() + scope: Scope = "ALL" + permission: str = "ALL" + block: bool = True + handlers: list = [] + temp: bool = False - def _default_parser(event: "Event", state: dict): - state[state.pop("_current_arg")] = event.message + _default_state: dict = {} + _default_parser: Optional[Callable[[Event, dict], None]] = None + _args_parser: Optional[Callable[[Event, dict], None]] = None - self._args_parser = _default_parser + def __init__(self): + self.handlers = self.handlers.copy() + self.state = self._default_state.copy() + self.parser = self._args_parser or self._default_parser - def __call__(self, func: Handler) -> Handler: - self.handlers.append(func) + @classmethod + def new(cls, + rule: Rule = Rule(), + scope: Scope = "ALL", + permission: str = "ALL", + block: bool = True, + handlers: list = [], + temp: bool = False, + *, + default_state: dict = {}, + default_parser: Optional[Callable[[Event, dict], None]] = None, + args_parser: Optional[Callable[[Event, dict], None]] = None): - # TODO: export some functions - func.args_parser = self.args_parser - func.receive = self.receive - func.got = self.got + # class NewMatcher(cls): + # rule: Rule = rule + # scope: Scope = scope + # permission: str = permission + # block: bool = block + # handlers: list = handlers + # temp: bool = temp + # _default_state = default_state + + NewMatcher = type( + "Matcher", (cls,), { + "rule": rule, + "scope": scope, + "permission": permission, + "block": block, + "handlers": handlers, + "temp": temp, + "_default_state": default_state, + "_default_parser": default_parser, + "_args_parser": args_parser, + }) + + return NewMatcher + + @classmethod + def args_parser(cls, func: Callable[[Event, dict], None]): + cls._default_parser = func return func - def args_parser(self, func): - self._args_parser = func - return func - - def receive(self): + @classmethod + def receive(cls): def _decorator(func: Handler) -> Handler: @wraps(func) - def _handler(event: "Event", state: dict): - # TODO: add tmp matcher to matcher tree - matcher = Matcher(user(event.user_id) & self.rule, - scope=self.scope, - permission=self.permission, - block=self.block, - handlers=self.handlers, - state=state, - temp=True) - matcher.args_parser(self._args_parser) - raise BlockedException + def _handler(event: Event, state: dict): + raise PausedException - self.handlers.append(_handler) + cls.handlers.append(_handler) return func return _decorator - def got(self, key, args_parser=None): + @classmethod + def got(cls, + key: str, + args_parser: Optional[Callable[[Event, dict], None]] = None): def _decorator(func: Handler) -> Handler: @wraps(func) - def _handler(event: "Event", state: dict): + def _handler(event: Event, state: dict): if key not in state: - state["_current_arg"] = key - - # TODO: add tmp matcher to matcher tree - matcher = copy.copy(self) + if state.get("__current_arg__", None) == key: + state[key] = event.message + del state["__current_arg__"] + return func(event, state) + state["__current_arg__"] = key + cls._args_parser = args_parser raise RejectedException + return func(event, state) - self.handlers.append(_handler) + cls.handlers.append(_handler) return func return _decorator - def finish(self): - # BlockedException用于阻止后续handler继续执行 - raise BlockedException + @classmethod + def finish(cls): + raise FinishedException - def reject(self): - # RejectedException用于阻止后续handler继续执行并将当前handler放回队列 + @classmethod + def reject(cls): raise RejectedException + async def run(self, event): + if not self.rule(event): + return + + try: + if self.parser: + await self.parser(event, state) # type: ignore + + for _ in range(len(self.handlers)): + handler = self.handlers.pop(0) + await handler(event, self.state) + except RejectedException: + # TODO: add tmp matcher to matcher tree + self.handlers.insert(handler, 0) + matcher = Matcher.new(self.rule, + self.scope, + self.permission, + self.block, + self.handlers, + temp=True, + default_state=self.state, + default_parser=self._default_parser, + args_parser=self._args_parser) + return + except PausedException: + # TODO: add tmp matcher to matcher tree + matcher = Matcher.new(self.rule, + self.scope, + self.permission, + self.block, + self.handlers, + temp=True, + default_state=self.state, + default_parser=self._default_parser, + args_parser=self._args_parser) + return + except FinishedException: + return + def on_message(rule: Rule, scope="ALL", @@ -104,23 +165,23 @@ def on_message(rule: Rule, block=True, *, handlers=[], - state={}, - temp=False) -> Matcher: + temp=False, + state={}) -> Type[Matcher]: # TODO: add matcher to matcher tree - return Matcher(rule, - scope, - permission, - block, - handlers=handlers, - state=state, - temp=temp) + return Matcher.new(rule, + scope, + permission, + block, + handlers=handlers, + temp=temp, + default_state=state) def on_startswith(msg, start: int = None, end: int = None, rule: Optional[Rule] = None, - **kwargs) -> Matcher: + **kwargs) -> Type[Matcher]: return on_message(startswith(msg, start, end) & rule, **kwargs) if rule else on_message( startswith(msg, start, end), **kwargs) @@ -129,7 +190,7 @@ def on_startswith(msg, def on_regex(pattern, flags: Union[int, re.RegexFlag] = 0, rule: Optional[Rule] = None, - **kwargs) -> Matcher: + **kwargs) -> Type[Matcher]: return on_message(regex(pattern, flags) & rule, **kwargs) if rule else on_message( regex(pattern, flags), **kwargs) diff --git a/nonebot/rule.py b/nonebot/rule.py index aa0361b8..4f18a383 100644 --- a/nonebot/rule.py +++ b/nonebot/rule.py @@ -1,11 +1,13 @@ import re -from typing import Union, Callable +from typing import Union, Callable, Optional + +from .event import Event class Rule: - def __init__(self, checker: Callable[["Event"], bool]): - self.checker = checker + def __init__(self, checker: Optional[Callable[[Event], bool]] = None): + self.checker = checker or (lambda event: True) def __call__(self, event): return self.checker(event)