From d80482b9249e8d1a96c99467d70b48f86e90cdcf Mon Sep 17 00:00:00 2001 From: AiCorein Date: Tue, 5 Mar 2024 22:51:43 +0800 Subject: [PATCH] Update to 2.2.0 version. --- README.md | 2 +- melobot/__init__.py | 18 +- melobot/core/dispatcher.py | 8 +- melobot/core/init.py | 4 +- melobot/core/linker.py | 12 +- melobot/core/responder.py | 8 +- melobot/meta.py | 2 +- melobot/models/__init__.py | 7 +- melobot/models/action.py | 17 +- melobot/models/base.py | 8 +- melobot/models/bot.py | 6 +- melobot/models/event.py | 65 ++++-- melobot/models/handler.py | 32 +-- melobot/models/ipc.py | 29 +-- melobot/models/plugin.py | 71 +++--- melobot/models/session.py | 447 ++++++++++++++++++++++++------------ melobot/types/__init__.py | 2 +- melobot/types/core.py | 17 +- melobot/types/exceptions.py | 5 + melobot/types/models.py | 42 +++- melobot/types/typing.py | 33 ++- melobot/types/utils.py | 83 +++++-- melobot/utils/checker.py | 12 +- melobot/utils/formatter.py | 6 +- melobot/utils/matcher.py | 48 +++- setup.py | 2 +- 26 files changed, 658 insertions(+), 328 deletions(-) diff --git a/README.md b/README.md index dbbf7263..85573b6b 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ pip install melobot ``` 版本支持: -- python >= 3.8 +- python >= 3.10 - platform == All(mac 平台未测试) - OneBot 标准 >= 11 diff --git a/melobot/__init__.py b/melobot/__init__.py index 40c7356f..71e185d0 100644 --- a/melobot/__init__.py +++ b/melobot/__init__.py @@ -9,8 +9,8 @@ from .models import ( AttrSessionRule, BotSession, + MessageEvent, MetaEvent, - MsgEvent, NoticeEvent, Plugin, PluginBus, @@ -19,10 +19,13 @@ RespEvent, RWController, bot, - event, finish, get_twin_event, + meta_event, + msg_event, msg_text, + notice_event, + req_evnt, send, send_hup, send_reply, @@ -34,7 +37,7 @@ BotEvent, BotLife, BotMatcher, - PriorityLevel, + PriorLevel, SessionRule, ShareObjArgs, User, @@ -81,7 +84,7 @@ def get_metainfo() -> MetaInfo: "session", "to_cq_arr", "MetaEvent", - "MsgEvent", + "MessageEvent", "NoticeEvent", "Plugin", "PluginBus", @@ -90,7 +93,10 @@ def get_metainfo() -> MetaInfo: "RespEvent", "RWController", "bot", - "event", + "msg_event", + "notice_event", + "req_evnt", + "meta_event", "msg_text", "finish", "get_twin_event", @@ -101,7 +107,7 @@ def get_metainfo() -> MetaInfo: "BotEvent", "BotLife", "BotMatcher", - "PriorityLevel", + "PriorLevel", "SessionRule", "ShareObjArgs", "User", diff --git a/melobot/core/dispatcher.py b/melobot/core/dispatcher.py index 87280550..6a1ad666 100644 --- a/melobot/core/dispatcher.py +++ b/melobot/core/dispatcher.py @@ -10,14 +10,14 @@ NoticeEventHandler, ReqEventHandler, ) -from ..types.core import IEventDispatcher +from ..types.core import AbstractDispatcher from ..types.exceptions import * from ..types.models import BotLife from ..types.typing import * from ..utils.logger import Logger -class BotDispatcher(IEventDispatcher): +class BotDispatcher(AbstractDispatcher): """ bot 调度模块。负责将传递的普通事件送入各事件总线 (接收的事件类型:消息、请求和通知) @@ -62,10 +62,10 @@ async def dispatch(self, event: BotEvent) -> None: 把事件分发到对应的事件总线 """ await self._ready_signal.wait() - await BotHookBus.emit(BotLife.EVENT_RECEIVED, event, wait=True) + await BotHookBus.emit(BotLife.EVENT_BUILT, event, wait=True) try: - permit_priority = PriorityLevel.MIN.value + permit_priority = PriorLevel.MIN.value handlers = self._handlers[event.type] for handler in handlers: # 事件处理器优先级不够,则不分配给它处理 diff --git a/melobot/core/init.py b/melobot/core/init.py index 58b7daf4..a4b647c6 100644 --- a/melobot/core/init.py +++ b/melobot/core/init.py @@ -18,7 +18,7 @@ from ..models.bot import BOT_PROXY, BotHookBus from ..models.ipc import PluginBus, PluginStore from ..models.plugin import Plugin -from ..types.core import IActionResponder +from ..types.core import AbstractResponder from ..types.exceptions import * from ..types.models import BotLife from ..types.typing import * @@ -90,7 +90,7 @@ def load( cls, plugin_target: Union[str, Type[Plugin]], logger: Logger, - responder: IActionResponder, + responder: AbstractResponder, ) -> Plugin: """ 加载插件 diff --git a/melobot/core/linker.py b/melobot/core/linker.py index 0d62461c..60432609 100644 --- a/melobot/core/linker.py +++ b/melobot/core/linker.py @@ -9,14 +9,14 @@ from ..models.action import BotAction from ..models.bot import BotHookBus from ..models.event import BotEventBuilder -from ..types.core import IActionSender, IEventDispatcher, IRespDispatcher +from ..types.core import AbstractDispatcher, AbstractSender from ..types.exceptions import * from ..types.models import BotLife from ..types.typing import * from ..utils.logger import Logger -class BotLinker(IActionSender): +class BotLinker(AbstractSender): """ Bot 连接模块通过连接适配器的代理,完成事件接收与行为发送。 """ @@ -43,13 +43,13 @@ def __init__( self._send_lock = aio.Lock() self._rest_time = send_interval self._pre_send_time = time.time() - self._common_dispatcher: IEventDispatcher - self._resp_dispatcher: IRespDispatcher + self._common_dispatcher: AbstractDispatcher + self._resp_dispatcher: AbstractDispatcher def bind( self, - common_dispatcher: IEventDispatcher, - resp_dispatcher: IRespDispatcher, + common_dispatcher: AbstractDispatcher, + resp_dispatcher: AbstractDispatcher, ) -> None: """ 绑定其他核心组件的方法。 diff --git a/melobot/core/responder.py b/melobot/core/responder.py index 7ab9d0bc..ea3887d3 100644 --- a/melobot/core/responder.py +++ b/melobot/core/responder.py @@ -4,13 +4,13 @@ from ..models.action import BotAction from ..models.event import RespEvent -from ..types.core import IActionResponder, IActionSender, IRespDispatcher +from ..types.core import AbstractDispatcher, AbstractResponder, AbstractSender from ..types.exceptions import * from ..types.typing import * from ..utils.logger import Logger -class BotResponder(IActionResponder, IRespDispatcher): +class BotResponder(AbstractResponder, AbstractDispatcher): """ bot 响应模块,是 action 发送方和 bot 连接模块的媒介。 提供 action 发送、响应回送功能 @@ -22,9 +22,9 @@ def __init__(self, logger: Logger) -> None: self.logger = logger self._ready_signal = aio.Event() - self._action_sender: IActionSender + self._action_sender: AbstractSender - def bind(self, action_sender: IActionSender) -> None: + def bind(self, action_sender: AbstractSender) -> None: """ 绑定其他核心组件的方法。独立出来,方便上层先创建实例再调用 """ diff --git a/melobot/meta.py b/melobot/meta.py index 8e94549d..278ab0ec 100644 --- a/melobot/meta.py +++ b/melobot/meta.py @@ -5,7 +5,7 @@ class MetaInfo: def __init__(self) -> None: - self.VER = "2.1.0" + self.VER = "2.2.0" self.PROJ_NAME = "MeloBot" self.PROJ_DESC = "A qbot module with friendly interface, session control and plugin-supported." self.PROJ_SRC = "https://github.com/aicorein/melobot" diff --git a/melobot/models/__init__.py b/melobot/models/__init__.py index 2e6f1990..e8c81b82 100644 --- a/melobot/models/__init__.py +++ b/melobot/models/__init__.py @@ -1,16 +1,19 @@ from .action import * from .base import RWController, get_twin_event, to_cq_arr from .bot import BOT_PROXY as bot -from .event import MetaEvent, MsgEvent, NoticeEvent, RequestEvent, RespEvent +from .event import MessageEvent, MetaEvent, NoticeEvent, RequestEvent, RespEvent from .ipc import PluginBus, PluginStore from .plugin import Plugin from .session import SESSION_LOCAL as session from .session import ( AttrSessionRule, BotSession, - event, finish, + meta_event, + msg_event, msg_text, + notice_event, + req_evnt, send, send_hup, send_reply, diff --git a/melobot/models/action.py b/melobot/models/action.py index 73dc5945..88bdf62f 100644 --- a/melobot/models/action.py +++ b/melobot/models/action.py @@ -3,6 +3,7 @@ from copy import deepcopy from ..types.exceptions import * +from ..types.models import Flagable from ..types.typing import * from ..utils.base import get_id from .event import * @@ -395,7 +396,7 @@ def custom_msg_node( msgs = temp ret = { "type": "node", - "data": {"name": sendName, "uin": str(sendId), "content": msgs}, + "data": {"name": sendName, "uin": sendId, "content": msgs}, } if seq: ret["data"]["seq"] = seq @@ -406,7 +407,7 @@ def refer_msg_node(msgId: int) -> MsgNodeDict: """ 引用消息节点构造方法 """ - return {"type": "node", "data": {"id": str(msgId)}} + return {"type": "node", "data": {"id": msgId}} class ActionPacker(ABC): @@ -420,7 +421,7 @@ def __init__(self) -> None: self.params: dict -class BotAction: +class BotAction(Flagable): """ Bot 行为类 """ @@ -431,6 +432,7 @@ def __init__( respWaited: bool = False, triggerEvent: BotEvent = None, ) -> None: + super().__init__() # 只有 action 对应的响应需要被等待单独处理时,才会生成 id self.resp_id: Union[str, None] = str(get_id()) if respWaited else None self.type: str = package.type @@ -461,6 +463,15 @@ def flatten(self, indent: int = None) -> str: """ return json.dumps(self.extract(), ensure_ascii=False, indent=indent) + def _fill_trigger(self, event: BotEvent) -> None: + """ + 后期指定触发 event + """ + if self.trigger is None: + self.trigger = event + return + raise BotActionError("action 已记录触发 event,拒绝再次记录") + class MsgPacker(ActionPacker): """ diff --git a/melobot/models/base.py b/melobot/models/base.py index 6429e3a9..64145ce0 100644 --- a/melobot/models/base.py +++ b/melobot/models/base.py @@ -50,12 +50,12 @@ def replace_func(m) -> str: .replace(",", ",") ) if val.isdigit(): - val = int(val) + data[name] = int(val) + continue try: - val = float(val) + data[name] = float(val) except Exception: - pass - data[name] = val + data[name] = val content.append({"type": cq_type, "data": data}) return content diff --git a/melobot/models/bot.py b/melobot/models/bot.py index 5931ae7b..72a8e536 100644 --- a/melobot/models/bot.py +++ b/melobot/models/bot.py @@ -7,7 +7,7 @@ from types import MethodType from ..meta import MODULE_MODE_FLAG, MODULE_MODE_SET -from ..types.core import IActionResponder +from ..types.core import AbstractResponder from ..types.exceptions import * from ..types.models import BotLife, HookRunnerArgs from ..types.typing import * @@ -43,10 +43,10 @@ class BotHookBus: v: [] for k, v in BotLife.__members__.items() } __logger: Logger - __responder: IActionResponder + __responder: AbstractResponder @classmethod - def _bind(cls, logger: Logger, responder: IActionResponder) -> None: + def _bind(cls, logger: Logger, responder: AbstractResponder) -> None: """ 初始化该类,绑定全局日志器和行为响应器 """ diff --git a/melobot/models/event.py b/melobot/models/event.py index 343281ea..8dbe1533 100644 --- a/melobot/models/event.py +++ b/melobot/models/event.py @@ -15,7 +15,7 @@ def build(cls, rawEvent: Union[dict, str]) -> BotEvent: etype = rawEvent.get("post_type") if etype in ("message_sent", "message"): - return MsgEvent(rawEvent) + return MessageEvent(rawEvent) elif etype == "request": return RequestEvent(rawEvent) elif etype == "notice": @@ -26,18 +26,18 @@ def build(cls, rawEvent: Union[dict, str]) -> BotEvent: return RespEvent(rawEvent) -class MsgEvent(BotEvent): +class MessageEvent(BotEvent): def __init__(self, rawEvent: dict) -> None: super().__init__(rawEvent) self.bot_id = rawEvent.get("self_id") self.id: int - self.sender: MsgEvent.Sender + self.sender: MessageEvent.Sender self.group_id: Union[int, None] # 使用 CQ 字符串编码的消息 self.raw_content: str # array 格式表示所有类型消息段 - self.content: dict + self.content: List[CQMsgDict] # 消息中所有文本消息段的合并字符串 self.text: str self.font: int @@ -66,9 +66,9 @@ def _init(self) -> None: self.temp_src = None temp_src = rawEvent.get("temp_source") if temp_src: - self.temp_src = MsgEvent._TEMP_SRC_MAP[temp_src] + self.temp_src = MessageEvent._TEMP_SRC_MAP[temp_src] - self.sender = MsgEvent.Sender( + self.sender = MessageEvent.Sender( rawEvent=rawEvent, isGroup=self.is_group(), isGroupAnonym=self.is_group_anonym(), @@ -78,13 +78,26 @@ def _init(self) -> None: if self.is_group(): self.group_id = rawEvent["group_id"] - def _format_to_array(self, content: Union[dict, str]) -> dict: + def _format_to_array(self, content: Union[list, str]) -> List[CQMsgDict]: if not isinstance(content, str): + for item in content: + if item["type"] == "text": + continue + for k, v in item["data"].items(): + if not isinstance(v, str): + continue + if v.isdigit(): + item["data"][k] = int(v) + continue + try: + item["data"][k] = float(v) + except Exception: + pass return content else: return to_cq_arr(content) - def _get_text(self, content: Union[dict, str]) -> bool: + def _get_text(self, content: List[CQMsgDict]) -> str: """ 获取消息中所有文本消息,返回合并字符串 """ @@ -94,21 +107,28 @@ def _get_text(self, content: Union[dict, str]) -> bool: text_list.append(item["data"]["text"]) return "".join(text_list) - def cq_get(self, cq_type: str, param: str) -> Union[List[Union[int, float, str]], None]: + def get_cq(self, cq_type: str) -> List[CQMsgDict]: """ - 从当前 event 中获取指定 cq 类消息的指定 param,以列表形式返回。 - 当没有对应类型的 cq 消息或者没有对应的 param,不会返回空列表,而是返回 None + 从当前 event 中获取指定类型的 cq 消息 dict """ - if isinstance(self.content, str): - pass - else: - results = [] - for item in self.content: - if item["type"] == cq_type: - val = item["data"].get(param) - if val is not None: - results.append(val) - return results if len(results) > 0 else None + return [item for item in self.content if item["type"] == cq_type] + + def get_cq_params(self, cq_type: str, param: str, type: Type[T] = None) -> List[T]: + """ + 从当前 event 中获取指定类型 cq 消息的指定 param,以列表形式返回。 + 当没有任何对应类型的 cq 消息时,为空列表。如果有对应类型 cq 消息, + 但是 param 不存在,则在列表中产生值 None + + 可以指定 type 来强制转换类型,不填则使用智能解析出的类型 + """ + res = [] + for item in self.content: + if item["type"] == cq_type: + val = item["data"].get(param) + res.append(val) + if type is not None: + res = list(map(lambda x: type(x), res)) + return res def is_private(self) -> bool: """是否为私聊消息(注意群临时会话属于该类别)""" @@ -434,6 +454,9 @@ def _init(self) -> None: self.notice_msg_id = rawEvent["message_id"] def is_group(self) -> bool: + """ + 是否是来自群的通知事件 + """ return "group_id" in self.raw.keys() def is_group_upload(self) -> bool: diff --git a/melobot/models/handler.py b/melobot/models/handler.py index b0ccc967..fa662c5c 100644 --- a/melobot/models/handler.py +++ b/melobot/models/handler.py @@ -1,13 +1,13 @@ import asyncio as aio import traceback -from ..types.core import IActionResponder +from ..types.core import AbstractResponder from ..types.exceptions import * from ..types.models import BotEvent, SessionRule from ..types.typing import * from ..types.utils import BotChecker, BotMatcher, BotParser, Logger from .action import msg_action -from .event import MsgEvent +from .event import MessageEvent from .session import SESSION_LOCAL, BotSession, BotSessionManager @@ -16,10 +16,10 @@ def __init__( self, executor: AsyncFunc[None], plugin: Any, - responder: IActionResponder, + responder: AbstractResponder, logger: Logger, checker: BotChecker, - priority: PriorityLevel, + priority: PriorLevel, timeout: float, set_block: bool, temp: bool, @@ -94,7 +94,7 @@ async def _run_on_ctx( if session._hup_signal.is_set(): BotSessionManager._rouse(session) - async def _run(self, event: MsgEvent) -> None: + async def _run(self, event: MessageEvent) -> None: """ 获取 session 然后准备运行 executor """ @@ -134,7 +134,7 @@ async def _run(self, event: MsgEvent) -> None: return BotSessionManager.recycle(session, alive=self._hold) - async def evoke(self, event: MsgEvent) -> bool: + async def evoke(self, event: MessageEvent) -> bool: """ 接收总线分发的事件的方法。返回是否决定处理的判断。 便于 disptacher 进行优先级阻断。校验通过会自动处理事件。 @@ -168,12 +168,12 @@ def __init__( self, executor: AsyncFunc[None], plugin: Any, - responder: IActionResponder, + responder: AbstractResponder, logger: Logger, matcher: BotMatcher = None, parser: BotParser = None, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: float = None, set_block: bool = False, temp: bool = False, @@ -208,7 +208,7 @@ def __init__( if self.matcher and self.parser: raise EventHandlerError("参数 matcher 和 parser 不能同时存在") - def _match_and_parse(self, event: MsgEvent) -> bool: + def _match_and_parse(self, event: MessageEvent) -> bool: """ 通过验权后,尝试对事件进行匹配。 有普通的匹配器(matcher)匹配,也可以使用解析器(parser)匹配 @@ -236,7 +236,7 @@ def _match_and_parse(self, event: MsgEvent) -> bool: return False return True - async def evoke(self, event: MsgEvent) -> bool: + async def evoke(self, event: MessageEvent) -> bool: """ 接收总线分发的事件的方法。返回是否决定处理的判断。 便于 disptacher 进行优先级阻断。校验通过会自动处理事件。 @@ -273,10 +273,10 @@ def __init__( self, executor: AsyncFunc[None], plugin: Any, - responder: IActionResponder, + responder: AbstractResponder, logger: Logger, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: float = None, set_block: bool = False, temp: bool = False, @@ -311,10 +311,10 @@ def __init__( self, executor: AsyncFunc[None], plugin: Any, - responder: IActionResponder, + responder: AbstractResponder, logger: Logger, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: float = None, set_block: bool = False, temp: bool = False, @@ -349,10 +349,10 @@ def __init__( self, executor: AsyncFunc[None], plugin: Any, - responder: IActionResponder, + responder: AbstractResponder, logger: Logger, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: float = None, set_block: bool = False, temp: bool = False, diff --git a/melobot/models/ipc.py b/melobot/models/ipc.py index ee3de02a..24ea5ff9 100644 --- a/melobot/models/ipc.py +++ b/melobot/models/ipc.py @@ -4,7 +4,7 @@ from functools import partial from types import MethodType -from ..types.core import IActionResponder +from ..types.core import AbstractResponder from ..types.exceptions import * from ..types.models import ShareCbArgs, SignalHandlerArgs from ..types.typing import * @@ -65,13 +65,10 @@ def _create_so( if namespace not in cls.__store__.keys(): cls.__store__[namespace] = {} obj = cls.__store__[namespace].get(id) - if obj is None: - obj = ShareObject(namespace, id) - cls.__store__[namespace][id] = obj - else: - raise ShareObjError( - f"命名空间 {namespace} 中已经存在 id 为 {id} 的共享对象" - ) + if obj is not None: + return + obj = ShareObject(namespace, id) + cls.__store__[namespace][id] = obj if property is not None: obj._fill_ref(lambda: getattr(plugin, property)) else: @@ -145,10 +142,10 @@ class PluginBus: __store__: Dict[str, Dict[str, PluginSignalHandler]] = {} __logger: Logger - __responder: IActionResponder + __responder: AbstractResponder @classmethod - def _bind(cls, logger: Logger, responder: IActionResponder) -> None: + def _bind(cls, logger: Logger, responder: AbstractResponder) -> None: """ 初始化该类,绑定全局日志器和行为响应器 """ @@ -185,11 +182,15 @@ def make_args(func: AsyncFunc[Any]) -> SignalHandlerArgs: if isinstance(callback, SignalHandlerArgs): raise PluginSignalError("已注册的信号处理方法不能再注册") if not iscoroutinefunction(callback): - raise PluginSignalError(f"信号处理方法 {callback.__name__} 必须为异步函数") + raise PluginSignalError( + f"信号处理方法 {callback.__name__} 必须为异步函数" + ) if ( isinstance(callback, partial) and isinstance(callback.func, MethodType) ) or isinstance(callback, MethodType): - raise PluginSignalError("callback 应该是 function,而不是 bound method。") + raise PluginSignalError( + "callback 应该是 function,而不是 bound method。" + ) handler = PluginSignalHandler(namespace, signal, callback, plugin=None) cls._register(namespace, signal, handler) @@ -245,7 +246,9 @@ async def emit( "在触发插件信号处理方法时传递原始 session,wait 参数需要为 True" ) if namespace not in cls.__store__.keys(): - raise PluginSignalError(f"插件信号命名空间 {namespace} 不存在,无法触发信号") + raise PluginSignalError( + f"插件信号命名空间 {namespace} 不存在,无法触发信号" + ) if signal not in cls.__store__[namespace].keys(): return diff --git a/melobot/models/plugin.py b/melobot/models/plugin.py index b937926b..a70727b5 100644 --- a/melobot/models/plugin.py +++ b/melobot/models/plugin.py @@ -3,7 +3,7 @@ from pathlib import Path from ..models.handler import EventHandler, EventHandlerArgs -from ..types.core import IActionResponder +from ..types.core import AbstractResponder from ..types.exceptions import * from ..types.models import ( HookRunnerArgs, @@ -13,7 +13,14 @@ SignalHandlerArgs, ) from ..types.typing import * -from ..types.utils import BotChecker, BotMatcher, BotParser, Logger, PrefixLogger +from ..types.utils import ( + BotChecker, + BotMatcher, + BotParser, + Logger, + LogicMode, + PrefixLogger, +) from ..utils.checker import ( AtChecker, FriendReqChecker, @@ -52,7 +59,7 @@ def __init__(self) -> None: self.__proxy: PluginProxy def __build( - self, root_path: Path, logger: Logger, responder: IActionResponder + self, root_path: Path, logger: Logger, responder: AbstractResponder ) -> None: """ 初始化当前插件 @@ -83,7 +90,9 @@ def __build( if isinstance(val, EventHandlerArgs): executor, handler_class, params = val if not iscoroutinefunction(executor): - raise PluginBuildError(f"事件处理器 {executor.__name__} 必须为异步方法") + raise PluginBuildError( + f"事件处理器 {executor.__name__} 必须为异步方法" + ) overtime_cb_maker, conflict_cb_maker = params[-1], params[-2] if overtime_cb_maker and not callable(overtime_cb_maker): raise PluginBuildError( @@ -100,7 +109,9 @@ def __build( elif isinstance(val, HookRunnerArgs): hook_func, type = val if not iscoroutinefunction(hook_func): - raise PluginBuildError(f"hook 方法 {hook_func.__name__} 必须为异步函数") + raise PluginBuildError( + f"hook 方法 {hook_func.__name__} 必须为异步函数" + ) runner = HookRunner(type, hook_func, self) BotHookBus._register(type, runner) @@ -115,7 +126,9 @@ def __build( elif isinstance(val, SignalHandlerArgs): func, namespace, signal = val if not iscoroutinefunction(func): - raise PluginBuildError(f"信号处理方法 {func.__name__} 必须为异步函数") + raise PluginBuildError( + f"信号处理方法 {func.__name__} 必须为异步函数" + ) handler = PluginSignalHandler(namespace, signal, func, self) PluginBus._register(namespace, signal, handler) @@ -127,7 +140,7 @@ def on_message( matcher: BotMatcher = None, parser: BotParser = None, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -169,7 +182,7 @@ def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: def on_every_message( cls, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -215,7 +228,7 @@ def on_at_qq( matcher: BotMatcher = None, parser: BotParser = None, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -262,9 +275,10 @@ def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: @classmethod def on_start_match( cls, - target: str, + target: Union[str, List[str]], + logic_mode: LogicMode = LogicMode.OR, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -279,7 +293,7 @@ def on_start_match( 使用该装饰器,将方法标记为字符串起始匹配的消息事件执行器。 必须首先含有以 target 起始的文本,才能被进一步处理 """ - start_matcher = StartMatch(target) + start_matcher = StartMatch(target, logic_mode) def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: return EventHandlerArgs( @@ -307,9 +321,10 @@ def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: @classmethod def on_contain_match( cls, - target: str, + target: Union[str, List[str]], + logic_mode: LogicMode = LogicMode.OR, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -324,7 +339,7 @@ def on_contain_match( 使用该装饰器,将方法标记为字符串包含匹配的消息事件执行器。 文本必须首先包含 target,才能被进一步处理 """ - contain_matcher = ContainMatch(target) + contain_matcher = ContainMatch(target, logic_mode) def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: return EventHandlerArgs( @@ -352,9 +367,10 @@ def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: @classmethod def on_full_match( cls, - target: str, + target: Union[str, List[str]], + logic_mode: LogicMode = LogicMode.OR, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -369,7 +385,7 @@ def on_full_match( 使用该装饰器,将方法标记为字符串相等匹配的消息事件执行器。 文本必须首先与 target 内容完全一致,才能被进一步处理 """ - full_matcher = FullMatch(target) + full_matcher = FullMatch(target, logic_mode) def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: return EventHandlerArgs( @@ -397,9 +413,10 @@ def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: @classmethod def on_end_match( cls, - target: str, + target: Union[str, List[str]], + logic_mode: LogicMode = LogicMode.OR, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -414,7 +431,7 @@ def on_end_match( 使用该装饰器,将方法标记为字符串结尾匹配的消息事件执行器。 文本必须首先以 target 结尾,才能被进一步处理 """ - end_matcher = EndMatch(target) + end_matcher = EndMatch(target, logic_mode) def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: return EventHandlerArgs( @@ -444,7 +461,7 @@ def on_regex_match( cls, target: str, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -488,7 +505,7 @@ def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: def on_request( cls, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -528,7 +545,7 @@ def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: def on_friend_request( cls, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -573,7 +590,7 @@ def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: def on_group_request( cls, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -638,7 +655,7 @@ def on_notice( "ALL", ], checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, @@ -683,7 +700,7 @@ def make_args(executor: AsyncFunc[None]) -> EventHandlerArgs: def on_meta_event( cls, checker: BotChecker = None, - priority: PriorityLevel = PriorityLevel.MEAN, + priority: PriorLevel = PriorLevel.MEAN, timeout: int = None, block: bool = False, temp: bool = False, diff --git a/melobot/models/session.py b/melobot/models/session.py index 53f54f73..48160ca2 100644 --- a/melobot/models/session.py +++ b/melobot/models/session.py @@ -7,7 +7,7 @@ from melobot.models.event import BotEvent from ..models.base import get_twin_event -from ..types.core import IActionResponder +from ..types.core import AbstractResponder from ..types.exceptions import * from ..types.models import SessionRule from ..types.typing import * @@ -20,16 +20,22 @@ class BotSession: Bot Session 类。不需要直接实例化,必须通过 BotSessionBuilder 构造。 """ - def __init__(self, responder: IActionResponder, space_tag: object = None) -> None: + def __init__( + self, + manager: Type["BotSessionManager"], + responder: AbstractResponder, + space_tag: object = None, + ) -> None: super().__init__() self.store = {} self.timestamp = time.time() self.hup_times: List[float] = [] self.events: List[ - Union[MsgEvent, RequestEvent, MetaEvent, RespEvent, NoticeEvent] + Union[MessageEvent, RequestEvent, MetaEvent, RespEvent, NoticeEvent] ] = [] - self._responder = responder + self._manager = manager + self._responder = responder # session 是否空闲的标志,由 BotSessionManager 修改和管理 self._free_signal = aio.Event() self._free_signal.set() @@ -41,18 +47,17 @@ def __init__(self, responder: IActionResponder, space_tag: object = None) -> Non # 用于标记该 session 属于哪个 session 空间,如果为 None 则表明是空 session 或是一次性 session # 其实这里如果传入 space_tag 则一定是所属 handler 的引用 self._space_tag: Union[object, None] = space_tag - # 所属 handler 的引用(和 space_tag 不一样,所有在 handler 中产生的 session,此属性必为非空) self._handler: object = None @property def event( self, - ) -> Union[MsgEvent, RequestEvent, MetaEvent, RespEvent, NoticeEvent, None]: + ) -> Union[MessageEvent, RequestEvent, MetaEvent, NoticeEvent]: try: return next(reversed(self.events)) except StopIteration: - return None + raise BotSessionError("当前 session 下不存在可用的 event") @property def last_hup(self) -> Union[float, None]: @@ -62,11 +67,11 @@ def last_hup(self) -> Union[float, None]: return None @property - def args(self) -> Union[Dict[str, List[Any]], List[Any], None]: + def args(self): if self._handler is not None and self.event._args_map is not None: args_group = self.event._args_map.get(self._handler.parser.id) if self._handler.parser.target is None: - return deepcopy({name: args.vals for name, args in args_group.items()}) + return deepcopy(args_group) for cmd_name in self._handler.parser.target: args = args_group.get(cmd_name) if args is not None: @@ -74,13 +79,13 @@ def args(self) -> Union[Dict[str, List[Any]], List[Any], None]: raise BotSessionError("尝试获取的命令解析参数不存在") return None - async def suspend(self, overtime: int = None) -> None: + async def hup(self, overtime: int = None) -> None: """ 当前 session 挂起(也就是所在方法的挂起)。直到满足同一 session_rule 的事件重新进入, session 所在方法便会被唤醒。但如果设置了超时时间且在唤醒前超时,则会强制唤醒 session, 并抛出 BotHupTimeout 异常 """ - BotSessionManager._hup(self) + self._manager._hup(self) if overtime is None: await self._awake_signal.wait() elif overtime <= 0: @@ -92,7 +97,7 @@ async def suspend(self, overtime: int = None) -> None: ) if self._awake_signal.is_set(): return - BotSessionManager._rouse(self) + self._manager._rouse(self) raise SessionHupTimeout("session 挂起超时") def destory(self) -> None: @@ -104,7 +109,7 @@ def destory(self) -> None: if self.event is None: pass else: - BotSessionManager._expire(self) + self._manager._expire(self) def store_get(self, key: object) -> object: return self.store[key] @@ -122,7 +127,7 @@ def store_clear(self) -> None: self.store.clear() # 不要更改这个方法下的所有 typing,否则会影响被装饰方法的 typing - def _launch(get_action): + def _action_launch(get_action): """ action 构建方法的装饰器, 在 action 构建后进行发送,以及完成响应等待 @@ -134,6 +139,9 @@ async def wrapper(self: "BotSession", *args, **kwargs): raise BotSessionError("session 已标记为过期,无法执行 action 操作") action: BotAction = await get_action(self, *args, **kwargs) + if len(self.events) > 0: + action._fill_trigger(self.event) + if action.resp_id is None: return await self._responder.take_action(action) else: @@ -143,14 +151,14 @@ async def wrapper(self: "BotSession", *args, **kwargs): """以下所有 action 方法虽然本身不是异步的,但不使用 async,返回值将没有注解""" - @_launch + @_action_launch async def custom_action(self, action: BotAction) -> Union[RespEvent, None]: """ 直接处理提供的 action """ return action - @_launch + @_action_launch async def send( self, content: Union[str, CQMsgDict, List[CQMsgDict]], @@ -169,13 +177,12 @@ async def send( self.event.sender.id, self.event.group_id, waitResp, - self.event, ) if cq_str: action = to_cq_str_action(action) return action - @_launch + @_action_launch async def custom_send( self, content: Union[str, CQMsgDict, List[CQMsgDict]], @@ -193,12 +200,12 @@ async def custom_send( raise BotActionError("为私聊时,构建 action 必须提供 userId 参数") if not isPrivate and groupId is None: raise BotActionError("为群聊时,构建 action 必须提供 groupId 参数") - action = msg_action(content, isPrivate, userId, groupId, waitResp, self.event) + action = msg_action(content, isPrivate, userId, groupId, waitResp, None) if cq_str: action = to_cq_str_action(action) return action - @_launch + @_action_launch async def send_forward( self, msgNodes: List[MsgNodeDict], @@ -219,13 +226,12 @@ async def send_forward( self.event.sender.id, self.event.group_id, waitResp, - self.event, ) if cq_str: action = to_cq_str_action(action) return action - @_launch + @_action_launch async def custom_send_forward( self, msgNodes: List[MsgNodeDict], @@ -239,30 +245,28 @@ async def custom_send_forward( 自定义发送转发消息。 cq_str 若开启,文本中若包含 cq 字符串,将会被解释 """ - action = forward_msg_action( - msgNodes, isPrivate, userId, groupId, waitResp, self.event - ) + action = forward_msg_action(msgNodes, isPrivate, userId, groupId, waitResp) if cq_str: action = to_cq_str_action(action) return action - @_launch + @_action_launch async def recall( self, msgId: int, waitResp: bool = False ) -> Union[RespEvent, None]: """ 撤回消息 """ - return msg_del_action(msgId, waitResp, self.event) + return msg_del_action(msgId, waitResp) - @_launch + @_action_launch async def get_msg(self, msgId: int) -> RespEvent: """ 获取消息信息 """ - return get_msg_action(msgId, True, self.event) + return get_msg_action(msgId, True) - @_launch + @_action_launch async def get_forward_msg( self, forwardId: str, @@ -270,18 +274,18 @@ async def get_forward_msg( """ 获取转发消息信息 """ - return get_forward_msg_action(forwardId, True, self.event) + return get_forward_msg_action(forwardId, True) - @_launch + @_action_launch async def mark_read( self, msgId: int, waitResp: bool = False ) -> Union[RespEvent, None]: """ 标记为已读 """ - return mark_msg_read_action(msgId, waitResp, self.event) + return mark_msg_read_action(msgId, waitResp) - @_launch + @_action_launch async def group_kick( self, groupId: int, @@ -292,9 +296,9 @@ async def group_kick( """ 群组踢人 """ - return group_kick_action(groupId, userId, laterReject, waitResp, self.event) + return group_kick_action(groupId, userId, laterReject, waitResp) - @_launch + @_action_launch async def group_ban( self, groupId: int, @@ -306,9 +310,9 @@ async def group_ban( 群组禁言。 duration 为 0 取消禁言 """ - return group_ban_action(groupId, userId, duration, waitResp, self.event) + return group_ban_action(groupId, userId, duration, waitResp) - @_launch + @_action_launch async def group_ban_anonymous( self, groupId: int, @@ -320,11 +324,9 @@ async def group_ban_anonymous( 群组匿名禁言。 无法取消禁言 """ - return group_anonym_ban_action( - groupId, anonymFlag, duration, waitResp, self.event - ) + return group_anonym_ban_action(groupId, anonymFlag, duration, waitResp) - @_launch + @_action_launch async def group_ban_all( self, groupId: int, @@ -334,9 +336,9 @@ async def group_ban_all( """ 群组全员禁言 """ - return group_whole_ban_action(groupId, enable, waitResp, self.event) + return group_whole_ban_action(groupId, enable, waitResp) - @_launch + @_action_launch async def group_leave( self, groupId: int, @@ -346,9 +348,9 @@ async def group_leave( """ 退出群组 """ - return group_leave_action(groupId, isDismiss, waitResp, self.event) + return group_leave_action(groupId, isDismiss, waitResp) - @_launch + @_action_launch async def group_sign( self, groupId: int, @@ -357,9 +359,9 @@ async def group_sign( """ 群组打卡 """ - return group_sign_action(groupId, waitResp, self.event) + return group_sign_action(groupId, waitResp) - @_launch + @_action_launch async def get_group( self, groupId: int, @@ -368,16 +370,16 @@ async def get_group( """ 获取群信息 """ - return get_group_info_action(groupId, noCache, True, self.event) + return get_group_info_action(groupId, noCache, True) - @_launch + @_action_launch async def get_groups(self) -> RespEvent: """ 获取 bot 加入的群列表 """ - return get_group_list_action(True, self.event) + return get_group_list_action(True) - @_launch + @_action_launch async def get_group_member( self, groupId: int, @@ -387,9 +389,9 @@ async def get_group_member( """ 获取群内单独一个群成员信息 """ - return get_group_member_info_action(groupId, userId, noCache, True, self.event) + return get_group_member_info_action(groupId, userId, noCache, True) - @_launch + @_action_launch async def get_group_members( self, groupId: int, @@ -398,9 +400,9 @@ async def get_group_members( """ 获取群成员列表 """ - return get_group_member_list_action(groupId, noCache, True, self.event) + return get_group_member_list_action(groupId, noCache, True) - @_launch + @_action_launch async def get_group_honor( self, groupId: int, @@ -411,9 +413,9 @@ async def get_group_honor( """ 获取群荣誉信息 """ - return get_group_honor_action(groupId, type, True, self.event) + return get_group_honor_action(groupId, type, True) - @_launch + @_action_launch async def get_group_file_sys( self, groupId: int, @@ -421,9 +423,12 @@ async def get_group_file_sys( """ 获取群文件系统信息 """ - return get_group_filesys_info_action(groupId, True, self.event) + return get_group_filesys_info_action( + groupId, + True, + ) - @_launch + @_action_launch async def get_group_root_files( self, groupId: int, @@ -431,16 +436,23 @@ async def get_group_root_files( """ 获取群根目录文件列表 """ - return get_group_root_files_action(groupId, True, self.event) + return get_group_root_files_action( + groupId, + True, + ) - @_launch + @_action_launch async def get_group_files_in_folder(self, groupId: int, folderId: str) -> RespEvent: """ 获取群子目录文件列表 """ - return get_group_files_byfolder_action(groupId, folderId, True, self.event) + return get_group_files_byfolder_action( + groupId, + folderId, + True, + ) - @_launch + @_action_launch async def get_group_file_url( self, groupId: int, fileId: str, fileTypeId: int ) -> RespEvent: @@ -448,16 +460,23 @@ async def get_group_file_url( 获取群文件资源链接。文件相关信息通过 `get_group_root_files` 或 `get_group_files` 的响应获得 """ - return get_group_file_url_action(groupId, fileId, fileTypeId, True, self.event) + return get_group_file_url_action( + groupId, + fileId, + fileTypeId, + True, + ) - @_launch + @_action_launch async def get_group_sys_msg(self) -> RespEvent: """ 获取群系统消息 """ - return get_group_sys_msg_action(True, self.event) + return get_group_sys_msg_action( + True, + ) - @_launch + @_action_launch async def get_group_notices( self, groupId: int, @@ -466,50 +485,74 @@ async def get_group_notices( 获取群公告。 群公告图片有 id,但暂时没有下载的方法 """ - return get_group_notice_action(groupId, True, self.event) + return get_group_notice_action( + groupId, + True, + ) - @_launch + @_action_launch async def get_group_records(self, msgSeq: int, groupId: int) -> RespEvent: """ 获取群消息历史记录 """ - return get_group_msg_history_action(msgSeq, groupId, True, self.event) + return get_group_msg_history_action( + msgSeq, + groupId, + True, + ) - @_launch + @_action_launch async def get_group_essences(self, groupId: int) -> RespEvent: """ 获取精华消息列表 """ - return get_group_essence_list_action(groupId, True, self.event) + return get_group_essence_list_action( + groupId, + True, + ) - @_launch + @_action_launch async def set_group_admin( self, groupId: int, userId: int, enable: bool, waitResp: bool = False ) -> Union[RespEvent, None]: """ 设置群管理员 """ - return set_group_admin_action(groupId, userId, enable, waitResp, self.event) + return set_group_admin_action( + groupId, + userId, + enable, + waitResp, + ) - @_launch + @_action_launch async def set_group_card( self, groupId: int, userId: int, card: str, waitResp: bool = False ) -> Union[RespEvent, None]: """ 设置群名片 """ - return set_group_card_action(groupId, userId, card, waitResp, self.event) + return set_group_card_action( + groupId, + userId, + card, + waitResp, + ) - @_launch + @_action_launch async def set_group_name( self, groupId: int, name: str, waitResp: bool = False ) -> Union[RespEvent, None]: """ 设置群名 """ - return set_group_name_action(groupId, name, waitResp, self.event) + return set_group_name_action( + groupId, + name, + waitResp, + ) - @_launch + @_action_launch async def set_group_title( self, groupId: int, @@ -522,10 +565,14 @@ async def set_group_title( 设置群头衔 """ return set_group_title_action( - groupId, userId, title, duration, waitResp, self.event + groupId, + userId, + title, + duration, + waitResp, ) - @_launch + @_action_launch async def process_group_add( self, addFlag: str, @@ -538,10 +585,14 @@ async def process_group_add( 处理加群请求 """ return set_group_add_action( - addFlag, addType, approve, rejectReason, waitResp, self.event + addFlag, + addType, + approve, + rejectReason, + waitResp, ) - @_launch + @_action_launch async def set_group_icon( self, groupId: int, file: str, cache: Literal[0, 1] = 0, waitResp: bool = False ) -> Union[RespEvent, None]: @@ -550,9 +601,14 @@ async def set_group_icon( 如本地路径为:`file:///C:/Users/Richard/Pictures/1.png`。 特别注意:目前此 API 在登录一段时间后会因 cookie 失效而失效 """ - return set_group_portrait_action(groupId, file, cache, waitResp, self.event) + return set_group_portrait_action( + groupId, + file, + cache, + waitResp, + ) - @_launch + @_action_launch async def set_group_notice( self, groupId: int, content: str, imageUrl: str = None, waitResp: bool = False ) -> Union[RespEvent, None]: @@ -560,36 +616,53 @@ async def set_group_notice( 发送群公告。 注意 `imageUrl` 只能为本地 url,示例:`file:///C:/users/15742/desktop/123.jpg` """ - return set_group_notice_action(groupId, content, imageUrl, waitResp, self.event) + return set_group_notice_action( + groupId, + content, + imageUrl, + waitResp, + ) - @_launch + @_action_launch async def set_group_essence( self, msgId: int, type: Literal["add", "del"], waitResp: bool = False ) -> Union[RespEvent, None]: """ 设置群精华消息 """ - return set_group_essence_action(msgId, type, waitResp, self.event) + return set_group_essence_action( + msgId, + type, + waitResp, + ) - @_launch + @_action_launch async def create_group_folder( self, groupId: int, folderName: str, waitResp: bool = False ) -> Union[RespEvent, None]: """ 创建群文件夹。注意:只能在根目录创建文件夹 """ - return create_group_folder_action(groupId, folderName, waitResp, self.event) + return create_group_folder_action( + groupId, + folderName, + waitResp, + ) - @_launch + @_action_launch async def delete_group_folder( self, groupId: int, folderId: str, waitResp: bool = False ) -> Union[RespEvent, None]: """ 删除群文件夹 """ - return delete_group_folder_action(groupId, folderId, waitResp, self.event) + return delete_group_folder_action( + groupId, + folderId, + waitResp, + ) - @_launch + @_action_launch async def delete_group_file( self, groupId: int, fileId: str, fileTypeId: int, waitResp: bool = False ) -> Union[RespEvent, None]: @@ -598,24 +671,31 @@ async def delete_group_file( `get_group_files` 的响应获得 """ return delete_group_file_action( - groupId, fileId, fileTypeId, waitResp, self.event + groupId, + fileId, + fileTypeId, + waitResp, ) - @_launch + @_action_launch async def get_friends(self) -> RespEvent: """ 获取好友列表 """ - return get_friend_list_action(True, self.event) + return get_friend_list_action( + True, + ) - @_launch + @_action_launch async def get_undirect_friends(self) -> RespEvent: """ 获取单向好友列表 """ - return get_undirect_friend_action(True, self.event) + return get_undirect_friend_action( + True, + ) - @_launch + @_action_launch async def get_user( self, userId: int, @@ -624,43 +704,58 @@ async def get_user( """ 获取用户信息。可以对陌生人或好友使用 """ - return get_stranger_info_action(userId, noCache, True, self.event) + return get_stranger_info_action( + userId, + noCache, + True, + ) - @_launch + @_action_launch async def process_friend_add( self, addFlag: str, approve: bool, remark: str, waitResp: bool = False ) -> Union[RespEvent, None]: """ 处理加好友。注意 remark 目前暂未实现 """ - return set_friend_add_action(addFlag, approve, remark, waitResp, self.event) + return set_friend_add_action( + addFlag, + approve, + remark, + waitResp, + ) - @_launch + @_action_launch async def delete_friend( self, userId: int, waitResp: bool = False ) -> Union[RespEvent, None]: """ 删除好友 """ - return delete_friend_action(userId, waitResp, self.event) + return delete_friend_action( + userId, + waitResp, + ) - @_launch + @_action_launch async def delete_undirect_friend( self, userId: int, waitResp: bool = False ) -> Union[RespEvent, None]: """ 删除单向好友 """ - return delete_undirect_friend_action(userId, waitResp, self.event) + return delete_undirect_friend_action( + userId, + waitResp, + ) - @_launch + @_action_launch async def get_login_info(self) -> RespEvent: """ 获得登录号信息 """ - return get_login_info_action(True, self.event) + return get_login_info_action(True) - @_launch + @_action_launch async def set_login_profile( self, nickname: str, @@ -674,38 +769,51 @@ async def set_login_profile( 设置登录号资料 """ return set_login_profile_action( - nickname, company, email, college, personalNote, waitResp, self.event + nickname, + company, + email, + college, + personalNote, + waitResp, ) - @_launch + @_action_launch async def check_send_image(self) -> RespEvent: """ 检查是否可以发送图片 """ - return check_send_image_action(True, self.event) + return check_send_image_action( + True, + ) - @_launch + @_action_launch async def check_send_audio(self) -> RespEvent: """ 检查是否可以发送语音 """ - return check_send_record_action(True, self.event) + return check_send_record_action( + True, + ) - @_launch + @_action_launch async def get_cq_status(self) -> RespEvent: """ 获取 go-cqhttp 状态 """ - return get_cq_status_action(True, self.event) + return get_cq_status_action( + True, + ) - @_launch + @_action_launch async def get_cq_version(self) -> RespEvent: """ 获取 go-cqhttp 版本信息 """ - return get_cq_version_action(True, self.event) + return get_cq_version_action( + True, + ) - @_launch + @_action_launch async def quick_handle( self, contextEvent: BotEvent, @@ -719,18 +827,20 @@ async def quick_handle( # return quick_handle_action( # contextEvent, # operation, - # waitResp, - # self.event + # waitResp # ) - @_launch + @_action_launch async def get_image(self, fileName: str) -> RespEvent: """ 获取图片信息 """ - return get_image_action(fileName, True, self.event) + return get_image_action( + fileName, + True, + ) - @_launch + @_action_launch async def download_file( self, fileUrl: str, @@ -752,10 +862,13 @@ async def download_file( ``` """ return download_file_action( - fileUrl, useThreadNum, headers, waitResp, self.event + fileUrl, + useThreadNum, + headers, + waitResp, ) - @_launch + @_action_launch async def ocr( self, image: str, @@ -763,9 +876,12 @@ async def ocr( """ 图片 OCR。image 为图片 ID """ - return ocr_action(image, True, self.event) + return ocr_action( + image, + True, + ) - @_launch + @_action_launch async def upload_file( self, isPrivate: bool, @@ -793,17 +909,19 @@ async def upload_file( groupId, groupFolderId, waitResp, - self.event, ) - @_launch + @_action_launch async def get_at_all_remain(self, groupId: int) -> RespEvent: """ 获取群 @全体成员 剩余次数 """ - return get_atall_remain_action(groupId, True, self.event) + return get_atall_remain_action( + groupId, + True, + ) - @_launch + @_action_launch async def get_online_clients( self, noCache: bool, @@ -811,9 +929,12 @@ async def get_online_clients( """ 获取当前账号在线客户端列表 """ - return get_online_clients_action(noCache, True, self.event) + return get_online_clients_action( + noCache, + True, + ) - @_launch + @_action_launch async def get_model_show( self, model: str, @@ -821,14 +942,21 @@ async def get_model_show( """ 获取在线机型 """ - return get_model_show_action(model, True, self.event) + return get_model_show_action( + model, + True, + ) - @_launch + @_action_launch async def set_model_show(self, model: str, modelShow: str) -> RespEvent: """ 设置在线机型 """ - return set_model_show_action(model, modelShow, True, self.event) + return set_model_show_action( + model, + modelShow, + True, + ) class BotSessionManager: @@ -953,7 +1081,7 @@ def recycle(cls, session: BotSession, alive: bool = False) -> None: @classmethod async def get( - cls, event: BotEvent, responder: IActionResponder, handler: object + cls, event: BotEvent, responder: AbstractResponder, handler: object ) -> Union[BotSession, None]: """ handler 内获取 session 方法。自动根据 handler._rule 判断是否需要映射到 session_space 进行存储。 @@ -974,7 +1102,7 @@ async def get( @classmethod def _make( - cls, event: BotEvent, responder: IActionResponder, handler: object = None + cls, event: BotEvent, responder: AbstractResponder, handler: object = None ) -> BotSession: """ 内部使用的创建 session 方法。如果 handler 为空,即缺乏 space_tag,则为一次性 session。 @@ -982,23 +1110,23 @@ def _make( """ if handler: if handler._rule: - session = BotSession(responder, handler) + session = BotSession(cls, responder, handler) session.events.append(event) cls.STORAGE[handler].add(session) return session - session = BotSession(responder) + session = BotSession(cls, responder) session.events.append(event) return session @classmethod - def make_empty(cls, responder: IActionResponder) -> BotSession: + def make_empty(cls, responder: AbstractResponder) -> BotSession: """ 创建空 session。即不含 event 和 space_tag 标记的 session """ - return BotSession(responder) + return BotSession(cls, responder) @classmethod - def make_temp(cls, event: BotEvent, responder: IActionResponder) -> BotSession: + def make_temp(cls, event: BotEvent, responder: AbstractResponder) -> BotSession: """ 创建一次性 session。确定无需 session 管理机制时可以使用。 否则一定使用 cls.get 方法 @@ -1007,7 +1135,7 @@ def make_temp(cls, event: BotEvent, responder: IActionResponder) -> BotSession: @classmethod async def _get_on_rule( - cls, event: BotEvent, responder: IActionResponder, handler: object + cls, event: BotEvent, responder: AbstractResponder, handler: object ) -> Union[BotSession, None]: """ 根据 handler 具体情况,从对应 session_space 中获取 session 或新建 session。 @@ -1131,7 +1259,7 @@ async def send_hup( 回复一条消息然后挂起 """ await SESSION_LOCAL.send(content, cq_str) - await SESSION_LOCAL.suspend(overtime) + await SESSION_LOCAL.hup(overtime) async def send_reply( @@ -1163,9 +1291,30 @@ async def finish( raise DirectRetSignal("事件处理方法被安全地递归 return,请无视这个异常") -def event(): +def msg_event() -> MessageEvent: + """ + 获取当前 session 上下文下的 MessageEvent + """ + return SESSION_LOCAL.event + + +def notice_event() -> NoticeEvent: + """ + 获取当前 session 上下文下的 NoticeEvent + """ + return SESSION_LOCAL.event + + +def req_evnt() -> RequestEvent: + """ + 获取当前 session 上下文下的 RequestEvent + """ + return SESSION_LOCAL.event + + +def meta_event() -> MetaEvent: """ - 获取当前 session 上下文下的 event + 获取当前 session 上下文下的 MetaEvent """ return SESSION_LOCAL.event diff --git a/melobot/types/__init__.py b/melobot/types/__init__.py index ce96d410..68909dce 100644 --- a/melobot/types/__init__.py +++ b/melobot/types/__init__.py @@ -1,4 +1,4 @@ from .exceptions import * from .models import BotEvent, BotLife, SessionRule, ShareObjArgs -from .typing import PriorityLevel, User +from .typing import PriorLevel, User from .utils import BotChecker, BotMatcher diff --git a/melobot/types/core.py b/melobot/types/core.py index 4ef69dd6..f98fa724 100644 --- a/melobot/types/core.py +++ b/melobot/types/core.py @@ -2,31 +2,26 @@ from asyncio import Future from ..models.action import BotAction -from ..models.event import BotEvent, MetaEvent, RespEvent +from ..models.event import BotEvent, RespEvent -class IRespDispatcher(ABC): +class AbstractSender(ABC): def __init__(self) -> None: super().__init__() @abstractmethod - async def dispatch(self, resp: RespEvent) -> None: + async def send(self, action: BotAction) -> None: pass -class IActionSender(ABC): +class AbstractResponder(ABC): def __init__(self) -> None: super().__init__() @abstractmethod - async def send(self, action: BotAction) -> None: + async def dispatch(self, resp: RespEvent) -> None: pass - -class IActionResponder(ABC): - def __init__(self) -> None: - super().__init__() - @abstractmethod async def take_action(self, action: BotAction) -> None: pass @@ -36,7 +31,7 @@ async def take_action_wait(self, action: BotAction) -> Future[RespEvent]: pass -class IEventDispatcher(ABC): +class AbstractDispatcher(ABC): def __init__(self) -> None: super().__init__() diff --git a/melobot/types/exceptions.py b/melobot/types/exceptions.py index 31ad2b2e..0f685c34 100644 --- a/melobot/types/exceptions.py +++ b/melobot/types/exceptions.py @@ -46,6 +46,11 @@ def __init__(self, msg: str): super().__init__(msg) +class TryFlagFailed(BotException): + def __init__(self, msg: str): + super().__init__(msg) + + class ShareObjError(BotException): def __init__(self, msg: str): super().__init__(msg) diff --git a/melobot/types/models.py b/melobot/types/models.py index 742781cc..ef8b3849 100644 --- a/melobot/types/models.py +++ b/melobot/types/models.py @@ -4,12 +4,50 @@ from .typing import * -class BotEvent(ABC): +class Flagable: + """ + 可标记对象 + """ + + def __init__(self) -> None: + self._flags_store: Dict[str, Dict[str, Any]] = None + + def mark(self, namespace: str, flag_name: str, val: Any = None) -> None: + """ + 为对象添加在指定命名空间下,名为 flag_name 的标记。 + 此后此对象会一直携带此标记,无法撤销。 + """ + if self._flags_store is None: + self._flags_store = {} + if self._flags_store.get(namespace) is None: + self._flags_store[namespace] = {} + if flag_name in self._flags_store[namespace].keys(): + raise TryFlagFailed( + f"对象不可被重复标记。在命名空间 {namespace} 中名为 {flag_name} 的标记已存在" + ) + self._flags_store[namespace][flag_name] = val + + def flag_check(self, namespace: str, flag_name: str, val: Any = None) -> bool: + """ + 检查此对象是否携带有指定的标记 + """ + self._flags_store = self._flags_store + if self._flags_store is None: + return False + if (flags := self._flags_store.get(namespace)) is None: + return False + if (flag := flags.get(flag_name, Void)) is Void: + return False + return flag is val if val is None else flag == val + + +class BotEvent(ABC, Flagable): """ Bot 事件类 """ def __init__(self, rawEvent: dict) -> None: + super().__init__() self.raw = rawEvent self._args_map: Dict[Any, Dict[str, ParseArgs]] = None @@ -69,7 +107,7 @@ class BotLife(Enum): CONNECTED = 2 BEFORE_CLOSE = 3 BEFORE_STOP = 4 - EVENT_RECEIVED = 5 + EVENT_BUILT = 5 ACTION_PRESEND = 6 diff --git a/melobot/types/typing.py b/melobot/types/typing.py index 2d00e445..ba319c52 100644 --- a/melobot/types/typing.py +++ b/melobot/types/typing.py @@ -40,9 +40,10 @@ "CQMsgDict", "MsgNodeDict", "User", - "PriorityLevel", + "PriorLevel", "ParseArgs", - "Null", + "Void", + "T", ) @@ -60,8 +61,24 @@ class MsgNodeDict(TypedDict): 消息节点 dict """ - type: str - data: Dict[str, Union[float, int, str]] + class CustomNodeData(TypedDict): + """ + 自定义消息节点 data dict + """ + + name: str + uin: int + content: List[CQMsgDict] + + class ReferNodeData(TypedDict): + """ + 引用消息节点 data dict + """ + + id: int + + type: Literal["node"] + data: Union[CustomNodeData, ReferNodeData] class ParseArgs: @@ -86,7 +103,7 @@ class User(int, Enum): BLACK = -1 -class PriorityLevel(int, Enum): +class PriorLevel(int, Enum): """ 优先级枚举。方便进行优先级比较,有 MIN, MAX, MEAN 三个枚举值 """ @@ -100,5 +117,9 @@ class PriorityLevel(int, Enum): AsyncFunc = Callable[..., Coroutine[Any, Any, T]] -class Null: +class Void: + """ + 表示无值,而不是 None 代表的“空值” + """ + pass diff --git a/melobot/types/utils.py b/melobot/types/utils.py index b0da4b62..bd0cccd7 100644 --- a/melobot/types/utils.py +++ b/melobot/types/utils.py @@ -9,8 +9,8 @@ from .typing import * _SysExcInfoType: TypeAlias = Union[ - tuple[type[BaseException], BaseException, Optional[TracebackType]], - tuple[None, None, None], + Tuple[Type[BaseException], BaseException, Optional[TracebackType]], + Tuple[None, None, None], ] _ExcInfoType: TypeAlias = Union[None, bool, _SysExcInfoType, BaseException] @@ -152,6 +152,35 @@ class LogicMode(Enum): NOT = 3 XOR = 4 + @classmethod + def calc(cls, logic: "LogicMode", v1: Any, v2: Any = None) -> bool: + if logic == LogicMode.AND: + return v1 and v2 + elif logic == LogicMode.OR: + return v1 or v2 + elif logic == LogicMode.NOT: + return not v1 + elif logic == LogicMode.XOR: + return v1 ^ v2 + + @classmethod + def seq_calc(cls, logic: "LogicMode", values: List[Any]) -> bool: + if len(values) <= 0: + return False + elif len(values) <= 1: + return bool(values[0]) + + idx = 0 + res = None + while idx < len(values): + if idx == 0: + res = cls.calc(logic, values[idx], values[idx + 1]) + idx += 1 + else: + res = cls.calc(logic, res, values[idx]) + idx += 1 + return res + class BotChecker(ABC): def __init__(self) -> None: @@ -159,12 +188,16 @@ def __init__(self) -> None: def __and__(self, other: "BotChecker") -> "WrappedChecker": if not isinstance(other, BotChecker): - raise BotCheckerError(f"联合检查器定义时出现了非检查器对象,其值为:{other}") + raise BotCheckerError( + f"联合检查器定义时出现了非检查器对象,其值为:{other}" + ) return WrappedChecker(LogicMode.AND, self, other) def __or__(self, other: "BotChecker") -> "WrappedChecker": if not isinstance(other, BotChecker): - raise BotCheckerError(f"联合检查器定义时出现了非检查器对象,其值为:{other}") + raise BotCheckerError( + f"联合检查器定义时出现了非检查器对象,其值为:{other}" + ) return WrappedChecker(LogicMode.OR, self, other) def __invert__(self) -> "WrappedMatcher": @@ -172,7 +205,9 @@ def __invert__(self) -> "WrappedMatcher": def __xor__(self, other: "BotChecker") -> "WrappedChecker": if not isinstance(other, BotChecker): - raise BotCheckerError(f"联合检查器定义时出现了非检查器对象,其值为:{other}") + raise BotCheckerError( + f"联合检查器定义时出现了非检查器对象,其值为:{other}" + ) return WrappedChecker(LogicMode.XOR, self, other) @abstractmethod @@ -194,14 +229,11 @@ def __init__( self.c1, self.c2 = checker1, checker2 def check(self, event: BotEvent) -> bool: - if self.mode == LogicMode.AND: - return self.c1.check(event) and self.c2.check(event) - elif self.mode == LogicMode.OR: - return self.c1.check(event) or self.c2.check(event) - elif self.mode == LogicMode.NOT: - return not self.c1.check(event) - elif self.mode == LogicMode.XOR: - return self.c1.check(event) ^ self.c2.check(event) + return LogicMode.calc( + self.mode, + self.c1.check(event), + self.c2.check(event) if self.c2 is not None else None, + ) class BotMatcher(ABC): @@ -210,12 +242,16 @@ def __init__(self) -> None: def __and__(self, other: "BotMatcher") -> "WrappedMatcher": if not isinstance(other, BotMatcher): - raise BotMatcherError(f"联合匹配器定义时出现了非匹配器对象,其值为:{other}") + raise BotMatcherError( + f"联合匹配器定义时出现了非匹配器对象,其值为:{other}" + ) return WrappedMatcher(LogicMode.AND, self, other) def __or__(self, other: "BotMatcher") -> "WrappedMatcher": if not isinstance(other, BotMatcher): - raise BotMatcherError(f"联合匹配器定义时出现了非匹配器对象,其值为:{other}") + raise BotMatcherError( + f"联合匹配器定义时出现了非匹配器对象,其值为:{other}" + ) return WrappedMatcher(LogicMode.OR, self, other) def __invert__(self) -> "WrappedMatcher": @@ -223,7 +259,9 @@ def __invert__(self) -> "WrappedMatcher": def __xor__(self, other: "BotMatcher") -> "WrappedMatcher": if not isinstance(other, BotMatcher): - raise BotMatcherError(f"联合匹配器定义时出现了非匹配器对象,其值为:{other}") + raise BotMatcherError( + f"联合匹配器定义时出现了非匹配器对象,其值为:{other}" + ) return WrappedMatcher(LogicMode.XOR, self, other) @abstractmethod @@ -245,14 +283,11 @@ def __init__( self.m1, self.m2 = matcher1, matcher2 def match(self, text: str) -> bool: - if self.mode == LogicMode.AND: - return self.m1.match(text) and self.m2.match(text) - elif self.mode == LogicMode.OR: - return self.m1.match(text) or self.m2.match(text) - elif self.mode == LogicMode.NOT: - return not self.m1.match(text) - elif self.mode == LogicMode.XOR: - return self.m1.match(text) ^ self.m2.match(text) + return LogicMode.calc( + self.mode, + self.m1.match(text), + self.m2.match(text) if self.m2 is not None else None, + ) class BotParser(ABC): diff --git a/melobot/utils/checker.py b/melobot/utils/checker.py index f6a54996..fb200e89 100644 --- a/melobot/utils/checker.py +++ b/melobot/utils/checker.py @@ -1,6 +1,6 @@ import re -from ..models.event import MetaEvent, MsgEvent, NoticeEvent, RequestEvent +from ..models.event import MessageEvent, MetaEvent, NoticeEvent, RequestEvent from ..types.exceptions import * from ..types.typing import * from ..types.utils import BotChecker @@ -27,7 +27,7 @@ def __init__( self.white_list = white_users if white_users is not None else [] self.black_list = black_users if black_users is not None else [] - def _get_level(self, event: MsgEvent) -> User: + def _get_level(self, event: MessageEvent) -> User: """ 获得事件对应的登记 """ @@ -44,7 +44,7 @@ def _get_level(self, event: MsgEvent) -> User: else: return User.USER - def check(self, event: MsgEvent) -> bool: + def check(self, event: MessageEvent) -> bool: """ 消息校验 """ @@ -71,7 +71,7 @@ def __init__( super().__init__(level, owner, super_users, white_users, black_users) self.white_group_list = white_groups if white_groups is not None else [] - def check(self, event: MsgEvent) -> bool: + def check(self, event: MessageEvent) -> bool: if not event.is_group(): return False if len(self.white_group_list) == 0: @@ -96,7 +96,7 @@ def __init__( ) -> None: super().__init__(level, owner, super_users, white_users, black_users) - def check(self, event: MsgEvent) -> bool: + def check(self, event: MessageEvent) -> bool: if not event.is_private(): return False return super().check(event) @@ -162,7 +162,7 @@ def __init__(self, qid: int = None) -> None: self.qid = str(qid) if qid is not None else None self._cq_at_regex = re.compile(r"\[CQ:at,qq=(\d+)?\]") - def check(self, event: MsgEvent) -> bool: + def check(self, event: MessageEvent) -> bool: """ 当 qid 为空时,只要有 at 消息就通过校验。 如果不为空,则必须出现指定 qid 的 at 消息 diff --git a/melobot/utils/formatter.py b/melobot/utils/formatter.py index 4b27a9a0..b78310f4 100644 --- a/melobot/utils/formatter.py +++ b/melobot/utils/formatter.py @@ -35,7 +35,7 @@ def __init__( verify: Callable[[Any], bool] = None, src_desc: str = None, src_expect: str = None, - default: Any = Null, + default: Any = Void, default_replace_flag: str = None, convert_tip_gen: TipGenerator = None, verify_tip_gen: TipGenerator = None, @@ -47,7 +47,7 @@ def __init__( self.src_expect = src_expect self.default = default self.default_replace_flag = default_replace_flag - if self.default is Null and self.default_replace_flag is not None: + if self.default is Void and self.default_replace_flag is not None: raise ArgFormatInitError( "初始化参数格式化器时,使用“默认值替换标记”必须同时设置默认值" ) @@ -57,7 +57,7 @@ def __init__( self.out_arglack_tip_gen = arglack_tip_gen def _get_val(self, args: ParseArgs, idx: int) -> Any: - if self.default is Null: + if self.default is Void: if args.vals is None or len(args.vals) < idx + 1: raise ArgLackError else: diff --git a/melobot/utils/matcher.py b/melobot/utils/matcher.py index 64731b3d..f0b69489 100644 --- a/melobot/utils/matcher.py +++ b/melobot/utils/matcher.py @@ -1,7 +1,7 @@ import re from ..types.typing import * -from ..types.utils import BotMatcher +from ..types.utils import BotMatcher, LogicMode class AlwaysMatch(BotMatcher): @@ -13,43 +13,67 @@ def match(self, text: str) -> bool: class StartMatch(BotMatcher): - def __init__(self, target: str) -> None: + def __init__( + self, target: Union[str, List[str]], mode: LogicMode = LogicMode.OR + ) -> None: super().__init__() self.target = target + self.mode = mode def match(self, text: str) -> bool: - return text[: len(self.target)] == self.target + if isinstance(self.target, str): + return text[: len(self.target)] == self.target + else: + res_seq = [text[: len(s)] == s for s in self.target] + LogicMode.seq_calc(self.mode, res_seq) class ContainMatch(BotMatcher): - def __init__(self, target: str, freq: int = 1) -> None: + def __init__( + self, target: Union[str, List[str]], mode: LogicMode = LogicMode.OR + ) -> None: super().__init__() self.target = target - self.freq = freq + self.mode = mode def match(self, text: str) -> bool: - if self.freq == 1: - return self.target in text + if isinstance(self.target, str): + return text in self.target else: - return len(re.findall(self.target, text)) == self.freq + res_seq = [text in s for s in self.target] + LogicMode.seq_calc(self.mode, res_seq) class EndMatch(BotMatcher): - def __init__(self, target: str) -> None: + def __init__( + self, target: Union[str, List[str]], mode: LogicMode = LogicMode.OR + ) -> None: super().__init__() self.target = target + self.mode = mode def match(self, text: str) -> bool: - return text[-len(self.target) :] == self.target + if isinstance(self.target, str): + return text[-len(self.target) :] == self.target + else: + res_seq = [text[-len(s) :] == s for s in self.target] + LogicMode.seq_calc(self.mode, res_seq) class FullMatch(BotMatcher): - def __init__(self, target: str) -> None: + def __init__( + self, target: Union[str, List[str]], mode: LogicMode = LogicMode.OR + ) -> None: super().__init__() self.target = target + self.mode = mode def match(self, text: str) -> bool: - return text == self.target + if isinstance(self.target, str): + return text == self.target + else: + res_seq = [text == s for s in self.target] + LogicMode.seq_calc(self.mode, res_seq) class RegexMatch(BotMatcher): diff --git a/setup.py b/setup.py index d0953e67..a3ea59bb 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ description=META_INFO.PROJ_DESC, long_description=readme, long_description_content_type="text/markdown", - python_requires=">=3.8", + python_requires=">=3.10", packages=[ "melobot", "melobot.core",