diff --git a/CHANGELOG.md b/CHANGELOG.md index 57fa510..7a70e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,23 @@ # 更新日志 +## [0.3.0] - 2022-11-21 +### 新增 + +`MiraiApi` 新增 `bot_list` 方法,用于获取已登录的 QQ 号。 + +### 修复 + +修复了 `MiraiApi` 不能多次 `close` 和 `connect` 的问题。 + +### 变更 + +依赖的 mirai-api-http 版本从 2.5.x 升级至 2.6.x。 + +由于升级后 mirai-api-http 的接口发生了变更,因此 `MiraiApi` 类的一些方法的参数发生了变化: + +- `message_from_id` +- `recall` +- `set_essence` + ## [0.2.0] - 2022-09-08 ### 新增 diff --git a/README.md b/README.md index 7d94ef5..b38cabb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # LightQ -![PyPI](https://img.shields.io/pypi/v/lightq?logo=pypi&logoColor=white) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lightq?logo=python&logoColor=white) ![mirai-api-http version](https://img.shields.io/badge/mirai--api--http-v2.5.2-blue) ![PyPI - License](https://img.shields.io/pypi/l/lightq) +![PyPI](https://img.shields.io/pypi/v/lightq?logo=pypi&logoColor=white) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lightq?logo=python&logoColor=white) ![mirai-api-http version](https://img.shields.io/badge/mirai--api--http-v2.6.2-blue) ![PyPI - License](https://img.shields.io/pypi/l/lightq) LightQ 是一个基于 [mirai-api-http](https://github.com/project-mirai/mirai-api-http) 的 QQ 机器人框架。 @@ -25,9 +25,9 @@ pip install . 环境要求: - Python 3.10 -- mirai-api-http 2.5.2 +- mirai-api-http 2.6.2 -LightQ 需要借助 Web API 调用 Mirai 的功能,因此请先安装并配置好 [Mirai Console Loader](https://github.com/iTXTech/mirai-console-loader) 和 [mirai-api-http](https://github.com/project-mirai/mirai-api-http) 插件: +LightQ 需要借助网络 API 调用 Mirai 的功能,因此请先安装并配置好 [Mirai Console Loader](https://github.com/iTXTech/mirai-console-loader) 和 [mirai-api-http](https://github.com/project-mirai/mirai-api-http) 插件: 1. 安装 [Mirai Console Loader (MCL)](https://github.com/iTXTech/mirai-console-loader)。 1. 在 MCL 中配置 QQ 账号和密码,确保能正常登录账号,中途可能需要使用 [TxCaptchaHelper](https://github.com/mzdluo123/TxCaptchaHelper) 应对滑动验证码。 @@ -61,12 +61,16 @@ if __name__ == '__main__': `message_handler` 装饰器将 `say_hello` 函数包装为一个 `MessageHandler` 对象,该消息处理器只会响应好友消息 `FriendMessage`。LightQ 还提供了 `event_handler` 和 `exception_handler` 装饰器,分别用于创建事件处理器和异常处理器。 -`bot.add_all(scan_handlers(__name__))` 的作用是获取当前模块中所有 public 的 handler,并将它们添加到 `bot` 中。注 1:[`__name__` 是 Python 中一个特殊的变量,表示当前模块的全限定名称](https://docs.python.org/zh-cn/3/reference/import.html#name__)。注 2:在 Python 中不以下划线开头的变量为模块的 public 成员,另一种做法是在模块中用 `__all__` 列出所有 public 成员的名字。 +`bot.add_all(scan_handlers(__name__))` 的作用是获取当前模块中所有 public 的 handler,并将它们添加到 `bot` 中。 + +- 注 1:[`__name__` 是 Python 中一个特殊的变量,表示当前模块的全限定名称](https://docs.python.org/zh-cn/3/reference/import.html#name__)。 +- 注 2:在 Python 中不以下划线开头的变量为模块的 public 成员,另一种做法是在模块中用 `__all__` 列出所有 public 成员的名字。 一个合法的 handler 函数需要返回 `str` 或 `MessageChain` 或 `None`。Handler 函数既可以是同步函数也可以是异步函数。 ```python -from lightq.entities import MessageChain, Plain +from lightq import message_handler +from lightq.entities import FriendMessage, MessageChain, Plain @message_handler(FriendMessage) async def say_hello() -> MessageChain: # 一个返回 MessageChain 的异步函数 @@ -93,7 +97,10 @@ def say_hello() -> str: 上面代码中的 `condition` 函数就是一个过滤器。`lightq.filters` 模块提供了一些现成的过滤器,可以直接使用。让我们再修改一下 `say_hello`,为它设置两个条件: ```python -from lightq import filters +from lightq import RecvContext, filters + +def condition(context: RecvContext) -> bool: + return str(context.data.message_chain) == 'Hello' @message_handler(FriendMessage, filters=[filters.from_user(987654321), condition]) def say_hello() -> str: @@ -102,6 +109,7 @@ def say_hello() -> str: ``` ### 参数解析 +#### 基于类型的参数解析 如果你用过 Spring Boot 之类的 Web 框架,对于参数解析这个概念应该不会陌生。LightQ 框架支持基于类型和基于函数两种参数解析机制。下面这个示例展示了如何使用基于类型的参数解析: @@ -115,7 +123,31 @@ def group_message_handler(chain: MessageChain): 注意到 `group_message_handler` 函数带有参数类型注解 `chain: MessageChain`,这个类型注解是不可或缺的。LightQ 框架使用 Python 的内省 (inspect) 机制获取 `chain` 参数的类型,接收到消息后解析出消息链对象,再自动地将消息链对象注入 `chain` 参数中。 -LightQ 框架支持自动解析的类型有:`Bot`、`RecvContext`、`ExceptionContext`、`MessageChain`、`Message` 及其子类、`Event` 及其子类、`Exception` 及其子类。参数解析机制也支持自定义类型,只需让你自己的类型继承 `lightq.framework` 中的 `FromContext` / `FromRecvContext` / `FromExceptionContext` 抽象类并重写对应的方法即可。 +参数解析机制的一个重要用途是在 handler 内获取 bot 的引用,并直接调用 bot 对象上的方法: + +```python +@event_handler(NudgeEvent) +async def nudge_response(event: NudgeEvent, bot: Bot): + """谁拍一拍我,我就拍一拍谁""" + if (event.subject.kind == 'Group' + and event.target == bot.bot_id + and event.from_id != bot.bot_id): + await bot.api.send_nudge(event.from_id, event.subject.id, 'Group') +``` + +LightQ 框架支持自动解析的类型有: + +- `Bot` +- `RecvContext` +- `ExceptionContext` +- `MessageChain` +- `Message` 及其子类 +- `Event` 及其子类 +- `Exception` 及其子类 + +参数解析机制也支持自定义类型,只需让你自己的类型继承 `lightq.framework` 中的 `FromContext` / `FromRecvContext` / `FromExceptionContext` 抽象类并重写对应的方法即可。 + +#### 基于函数的参数解析 基于类型的参数解析无法覆盖所有场景,例如:希望从群组消息中解析出群号和发送者的 QQ 号,但二者皆为 `int` 类型,仅凭类型无法区分。此时需要使用基于函数的参数解析,请看如下例子: @@ -133,18 +165,6 @@ def group_message_handler(chain: MessageChain, group_id: int, member_id: int): 使用 `resolve` 装饰器时,若以普通方式传参(上面的 `@resolve(resolvers.group_id)`),则根据解析器的 `__name__` 属性注入同名的参数([Python 函数的 `__name__` 属性默认为该函数的名字](https://docs.python.org/zh-cn/3/library/stdtypes.html#definition.__name__));若以命名参数的方式传参(上面的 `@resolve(member_id=resolvers.sender_id)`),则表示手动指定注入参数。 -参数解析机制的一个重要用途是在 handler 内获取 bot 的引用,并直接调用 bot 对象上的方法。详见下述示例: - -```python -@event_handler(NudgeEvent) -async def nudge_response(event: NudgeEvent, bot: Bot): - """谁拍一拍我,我就拍一拍谁""" - if (event.subject.kind == 'Group' - and event.target == bot.bot_id - and event.from_id != bot.bot_id): - await bot.api.send_nudge(event.from_id, event.subject.id, 'Group') -``` - 本节的示例代码放在 [examples/resolver_example.py](./examples/resolver_example.py) 中。 ### 正则表达式 diff --git a/pyproject.toml b/pyproject.toml index 0e2dacd..223096d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "lightq" -version = "0.2.1.dev0" +version = "0.3.0" authors = [{ name = "ZHB", email = "zhb2000@whu.edu.cn" }] description = "An elegant QQ bot framework based on mirai-api-http." readme = "README.md" diff --git a/src/lightq/api.py b/src/lightq/api.py index d67a10e..37f491a 100644 --- a/src/lightq/api.py +++ b/src/lightq/api.py @@ -127,15 +127,17 @@ def session_key(self) -> str | None: return self.__session_key async def send(self, data: dict[str, Any]) -> dict[str, Any]: """ Mirai-api-http 传入格式: - :: - { - "syncId": 123, // 消息同步的字段 - "command": "sendFriendMessage", // 命令字 - "subCommand": null, // 子命令字, 可空 - "content": {} // 命令的数据对象, 与通用接口定义相同 - } - ``syncId`` 由 ``send`` 方法自动生成,无需放在 JSON 中。 + ```json + { + "syncId": 123, // 消息同步的字段 + "command": "sendFriendMessage", // 命令字 + "subCommand": null, // 子命令字, 可空 + "content": {} // 命令的数据对象, 与通用接口定义相同 + } + ``` + + JSON 的 `syncId` 字段由 `send` 方法自动生成,无需传入。 :returns: 若状态码为 0 则将响应的 JSON 返回 :raises: @@ -162,13 +164,15 @@ async def send(self, data: dict[str, Any]) -> dict[str, Any]: async def recv(self) -> Message | Event | SyncMessage | UnsupportedEntity: """ Mirai-api-http 推送格式: - :: - { - "syncId": "123", // 消息同步的字段 - "data": {} // 推送消息内容, 与通用接口定义相同 - } - ``recv`` 方法返回其中的 ``data`` 部分。 + ```json + { + "syncId": "123", // 消息同步的字段 + "data": {} // 推送消息内容, 与通用接口定义相同 + } + ``` + + 本方法会返回 JSON 的 `data` 部分。 :raises websockets.exception.WebSocketException: WebSocket 连接被关闭或出错时抛出 """ @@ -211,7 +215,7 @@ async def __working_method(self): await ws.close() # the close method is idempotent async def connect(self): - """The ``connect`` method is idempotent.""" + """与 mirai-api-http 建立连接。如果连接已经建立,则什么也不做。""" if self.__ws is not None: return encoded_key = urllib.parse.quote_plus(self.verify_key) @@ -229,7 +233,7 @@ def remove_working_task(task): self.__working_task.add_done_callback(remove_working_task) async def close(self): - """The ``close`` method is idempotent.""" + """断开与 mirai-api-http 的连接。如果连接已经断开,则什么也不做。""" if self.__ws is None: return await self.__ws.close() @@ -267,10 +271,10 @@ async def send_command( async def message_from_id(self, message_id: int, friend_or_group_id: int) -> Message | None: """ - 通过 message id 获取消息,当该 message id 没有被缓存或缓存失效时返回 None + 通过 message id 获取消息。若该 message id 没有被缓存或缓存失效则返回 `None`。 :param message_id: 获取消息的 message id - :param friend_or_group_id: 好友 QQ 号或群 id + :param friend_or_group_id: 好友 QQ 号或群号 """ try: return Message.from_json((await self.send_command('messageFromId', { diff --git a/src/lightq/framework/_bot.py b/src/lightq/framework/_bot.py index 18caff6..28fb46a 100644 --- a/src/lightq/framework/_bot.py +++ b/src/lightq/framework/_bot.py @@ -120,6 +120,7 @@ async def run(self): context = RecvContext(self, data) background_func = self.__make_background_func(context) self.create_task(background_func()) + await asyncio.sleep(0) finally: await self.close()