diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 8fdc2d9a4dde..bd20695a2419 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -2,7 +2,7 @@ name: Build API Doc on: pull_request: - types: [ opened, synchronize, reopened ] + types: [opened, synchronize, reopened] jobs: build: @@ -30,7 +30,7 @@ jobs: - name: Set up dependencies run: | - poetry install + poetry install -E all - name: Build Doc run: poetry run sphinx-build -M markdown ./docs_build ./build diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 971c8b0e7503..97d3ac9b38d5 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -198,6 +198,10 @@ module.exports = context => ({ title: "nonebot.drivers.fastapi 模块", path: "drivers/fastapi" }, + { + title: "nonebot.drivers.quart 模块", + path: "drivers/quart" + }, { title: "nonebot.adapters 模块", path: "adapters/" diff --git a/docs/advanced/export-and-require.md b/docs/advanced/export-and-require.md index 832b0e7573af..cb24c428e3f0 100644 --- a/docs/advanced/export-and-require.md +++ b/docs/advanced/export-and-require.md @@ -1 +1,117 @@ # 跨插件访问 + +由于 `nonebot2` 独特的插件加载机制,在使用 python 原有的 import 机制来进行插件之间的访问时,很可能会有奇怪的或者意料以外的情况发生。为了避免这种情况的发生,您可以有两种方法来实现跨插件访问: + +1. 将插件间的要使用的公共代码剥离出来,作为公共文件或者文件夹,提供给插件加以调用。 +2. 使用 `nonebot2` 提供的 `export` 和 `require` 机制,来实现插件间的互相调用。 + +第一种方法比较容易理解和实现,这里不再赘述,但需要注意的是,请不要将公共文件或者公共文件夹作为**插件**被 `nonebot2` 加载。 + +下面将介绍第二种方法—— `export` 和 `require` 机制: + +## 使用 export and require + +现在,假定有两个插件 `pluginA` 和 `pluginB`,需要在 `pluginB` 中调用 `pluginA` 中的一个变量 `varA` 和一个函数 `funcA`。 + +在上面的条件中涉及到了两种操作:一种是在 `pluginA` 的 `导出对象` 操作;而另一种是在 `pluginB` 的 `导入对象` 操作。在 `nonebot2` 中,`导出对象` 的操作用 `export` 机制来实现,`导入对象` 的操作用 `require` 机制来实现。下面,我们将逐一进行介绍。 + +:::warning 警告 + +使用这个方法进行跨插件访问时,**需要先加载`导出对象`的插件,再加载`导入对象`的插件。** + +::: + +### 使用 export + +在 `pluginA` 中,我们调用 `export` 机制 `导出对象`。 + +在 `export` 机制调用前,我们需要保证导出的对象已经被定义,比如: + +```python +varA = "varA" + + +def funcA(): + return "funcA" +``` + +在确保定义之后,我们可以从 `nonebot.plugin` 导入 `export()` 方法, `export()` 方法会返回一个特殊的字典 `export`: + +```python +from nonebot.plugin import export + +export=export() +``` + +这个字典可以用来装载导出的对象,它的 key 是对象导出后的命名,value 是对象本身,我们可以直接创建新的 `key` - `value` 对导出对象: + +```python +export.vA = varA +export.fA = funcA +``` + +除此之外,也支持 `嵌套` 导出对象: + +```python +export.sub.vA = varA +export.sub.fA = funcA +``` + +特别地,对于 `函数对象` 而言,`export` 支持用 `装饰器` 的方法来导出,因此,我们可以这样定义 `funcA`: + +```python +@export.sub +def funcA(): + return "funcA" +``` + +或者: + +```python +@export +def funcA(): + return "funcA" +``` + +通过 `装饰器` 的方法导出函数时,命名固定为函数的命名,也就是说,上面的两个例子等同于: + +```python +export.sub.funcA = funcA + +export.funcA = funcA +``` + +这样,我们就成功导出 `varA` 和 `funcA` 对象了。 + +下面我们将介绍如何在 `pluginB` 中导入这些对象。 + +### 使用 require + +在 `pluginB` 中,我们调用 `require` 机制 `导入对象`。 + +:::warning 警告 + +在导入来自其他插件的对象时, 请确保导出该对象的插件在引用该对象的插件之前加载。如果该插件并未被加载,则会尝试加载,加载失败则会返回 `None`。 + +::: + +我们可以从 `nonebot.plugin` 中导入 `require()` 方法: + +```python +from nonebot.plugin import require +``` + +`require()` 方法的参数是插件名, 它会返回在指定插件中,用 `export()` 方法创建的字典。 + +```python +require_A = require('pluginA') +``` + +在之前,这个字典已经存入了 `'vA'` - `varA`, `'fA'` - `funcA` 或 `'funcA'` - `funcA` 这样的 `key` - `value` 对。因此在这里我们直接用 `属性` 的方法来获取导入对象: + +```python +varA = require_A.vA +funcA = require_A.fA or require_A.funcA +``` + +这样,我们就在 `pluginB` 中成功导入了 `varA` 和 `funcA` 对象了。 diff --git a/docs/api/README.md b/docs/api/README.md index 36e9803e56f4..e12dd0ff913d 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -43,6 +43,9 @@ * [nonebot.drivers.fastapi](drivers/fastapi.html) + * [nonebot.drivers.quart](drivers/quart.html) + + * [nonebot.adapters](adapters/) diff --git a/docs/api/adapters/mirai.md b/docs/api/adapters/mirai.md index b627f72d6e43..89ee9c0fd9a4 100644 --- a/docs/api/adapters/mirai.md +++ b/docs/api/adapters/mirai.md @@ -965,15 +965,40 @@ CQHTTP 协议 MessageSegment 适配。具体方法参考 [mirai-api-http 消息 基类:[`nonebot.adapters.Message`](README.md#nonebot.adapters.Message) -Mirai 协议 Messaqge 适配 +Mirai 协议 Message 适配 由于Mirai协议的Message实现较为特殊, 故使用MessageChain命名 +### `reduce()` + + +* **说明** + + 忽略为空的消息段, 合并相邻的纯文本消息段 + + + ### `export()` 导出为可以被正常json序列化的数组 + +### `extract_first(*type)` + + +* **说明** + + 弹出该消息链的第一个消息 + + + +* **参数** + + + * \*type: MessageType: 指定的消息类型, 当指定后如类型不匹配不弹出 + + # NoneBot.adapters.mirai.utils 模块 @@ -1070,20 +1095,6 @@ mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段 > * `MEMBER`: 普通群成员 -## _class_ `MessageChain` - -基类:[`nonebot.adapters.Message`](README.md#nonebot.adapters.Message) - -Mirai 协议 Messaqge 适配 - -由于Mirai协议的Message实现较为特殊, 故使用MessageChain命名 - - -### `export()` - -导出为可以被正常json序列化的数组 - - ## _class_ `MessageEvent` 基类:`nonebot.adapters.mirai.event.base.Event` diff --git a/docs/api/drivers/quart.md b/docs/api/drivers/quart.md new file mode 100644 index 000000000000..068769e0a0dc --- /dev/null +++ b/docs/api/drivers/quart.md @@ -0,0 +1,62 @@ +--- +contentSidebar: true +sidebarDepth: 0 +--- + +# NoneBot.drivers.quart 模块 + +## Quart 驱动适配 + +后端使用方法请参考: [Quart 文档](https://pgjones.gitlab.io/quart/index.html) + + +## _class_ `Driver` + +基类:[`nonebot.drivers.Driver`](README.md#nonebot.drivers.Driver) + +Quart 驱动框架 + + +* **上报地址** + + + * `/{adapter name}/http`: HTTP POST 上报 + + + * `/{adapter name}/ws`: WebSocket 上报 + + + +### _property_ `type` + +驱动名称: `quart` + + +### _property_ `server_app` + +`Quart` 对象 + + +### _property_ `asgi` + +`Quart` 对象 + + +### _property_ `logger` + +fastapi 使用的 logger + + +### `on_startup(func)` + +参考文档: [Startup and Shutdown](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html) + + +### `on_shutdown(func)` + +参考文档: [Startup and Shutdown](https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html) + + +### `run(host=None, port=None, *, app=None, **kwargs)` + +使用 `uvicorn` 启动 Quart diff --git a/docs/guide/creating-a-matcher.md b/docs/guide/creating-a-matcher.md index ac74f6c1d98b..83debe13a073 100644 --- a/docs/guide/creating-a-matcher.md +++ b/docs/guide/creating-a-matcher.md @@ -123,7 +123,7 @@ async def async_checker(bot: Bot, event: Event, state: T_State) -> bool: def sync_checker(bot: Bot, event: Event, state: T_State) -> bool: return True -def check(arg1, args2): +def check(arg1, arg2): async def _checker(bot: Bot, event: Event, state: T_State) -> bool: return bool(arg1 + arg2) diff --git a/docs/guide/loading-a-plugin.md b/docs/guide/loading-a-plugin.md index f026bbe05d6e..e3c7af2f3995 100644 --- a/docs/guide/loading-a-plugin.md +++ b/docs/guide/loading-a-plugin.md @@ -6,12 +6,15 @@ 在 `bot.py` 文件中添加以下行: -```python{5} +```python{8} import nonebot +from nonebot.adapters.cqhttp import Bot nonebot.init() -# 加载 nonebot 内置插件 -nonebot.load_builtin_plugins() + +driver = nonebot.get_driver() +driver.register_adapter("cqhttp", Bot) # 注册 CQHTTP 的 Adapter +nonebot.load_builtin_plugins() # 加载 nonebot 内置插件 app = nonebot.get_asgi() @@ -19,6 +22,12 @@ if __name__ == "__main__": nonebot.run() ``` +::: warning +目前, 内建插件仅支持 CQHTTP 的 Adapter + +如果您使用的是其他 Adapter, 请移步该 Adapter 相应的文档 +::: + 这将会加载 nonebot 内置的插件,它包含: - 命令 `say`:可由**superuser**使用,可以将消息内容由特殊纯文本转为富文本 diff --git a/docs/guide/mirai-guide.md b/docs/guide/mirai-guide.md index 403e55e0952a..c22631e0f1a2 100644 --- a/docs/guide/mirai-guide.md +++ b/docs/guide/mirai-guide.md @@ -193,3 +193,36 @@ Mirai-API-HTTP 的适配器以 [AGPLv3 许可](https://opensource.org/licenses/A ``` 恭喜你, 你的配置已经成功! + +现在, 我们可以写一个简单的插件来测试一下 + +```python +from nonebot.plugin import on_keyword, on_command +from nonebot.rule import to_me +from nonebot.adapters.mirai import Bot, MessageEvent + +message_test = on_keyword({'reply'}, rule=to_me()) + + +@message_test.handle() +async def _message(bot: Bot, event: MessageEvent): + text = event.get_plaintext() + await bot.send(event, text, at_sender=True) + + +command_test = on_command('miecho') + + +@command_test.handle() +async def _echo(bot: Bot, event: MessageEvent): + text = event.get_plaintext() + await bot.send(event, text, at_sender=True) +``` + +它具有两种行为 + +- 在指定机器人,即私聊、群聊内@机器人、群聊内称呼机器人昵称的情况下 (即 [Rule: to_me](../api/rule.md#to-me)), 如果消息内包含 `reply` 字段, 则该消息会被机器人重复一次 + +- 在执行指令`miecho xxx`时, 机器人会发送回参数`xxx` + +至此, 你已经初步掌握了如何使用 Mirai Adapter diff --git a/docs_build/README.rst b/docs_build/README.rst index 4a27304130e3..0e029d9b7e32 100644 --- a/docs_build/README.rst +++ b/docs_build/README.rst @@ -15,6 +15,7 @@ NoneBot Api Reference - `nonebot.exception `_ - `nonebot.drivers `_ - `nonebot.drivers.fastapi `_ + - `nonebot.drivers.quart `_ - `nonebot.adapters `_ - `nonebot.adapters.cqhttp `_ - `nonebot.adapters.ding `_ diff --git a/docs_build/drivers/quart.rst b/docs_build/drivers/quart.rst new file mode 100644 index 000000000000..189dd478e92f --- /dev/null +++ b/docs_build/drivers/quart.rst @@ -0,0 +1,12 @@ +--- +contentSidebar: true +sidebarDepth: 0 +--- + +NoneBot.drivers.quart 模块 +========================== + +.. automodule:: nonebot.drivers.quart + :members: + :private-members: + :show-inheritance: \ No newline at end of file diff --git a/nonebot/adapters/cqhttp/bot.py b/nonebot/adapters/cqhttp/bot.py index 62fb4aad8fde..3d4401276b65 100644 --- a/nonebot/adapters/cqhttp/bot.py +++ b/nonebot/adapters/cqhttp/bot.py @@ -425,7 +425,8 @@ async def send(self, - ``NetworkError``: 网络错误 - ``ActionFailed``: API 调用失败 """ - message = escape(message) if isinstance(message, str) else message + message = escape(message, escape_comma=False) if isinstance( + message, str) else message msg = message if isinstance(message, Message) else Message(message) at_sender = at_sender and getattr(event, "user_id", None) diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index 0a1262c71c7e..4f5cb196ffdb 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -1,8 +1,7 @@ from datetime import datetime, timedelta -from functools import wraps from io import BytesIO from ipaddress import IPv4Address -from typing import (Any, Dict, List, NoReturn, Optional, Tuple, Union) +from typing import Any, Dict, List, NoReturn, Optional, Tuple, Union import httpx @@ -10,15 +9,12 @@ from nonebot.config import Config from nonebot.drivers import Driver, WebSocket from nonebot.exception import ApiNotAvailable, RequestDenied -from nonebot.log import logger -from nonebot.message import handle_event from nonebot.typing import overrides -from nonebot.utils import escape_tag from .config import Config as MiraiConfig from .event import Event, FriendMessage, GroupMessage, TempMessage from .message import MessageChain, MessageSegment -from .utils import catch_network_error, argument_validation, check_tome, Log +from .utils import Log, argument_validation, catch_network_error, process_event class SessionManager: @@ -212,20 +208,15 @@ def register(cls, driver: "Driver", config: "Config"): async def handle_message(self, message: dict): Log.debug(f'received message {message}') try: - await handle_event( + await process_event( bot=self, - event=await check_tome( - bot=self, - event=Event.new({ - **message, - 'self_id': self.self_id, - }), - ), + event=Event.new({ + **message, + 'self_id': self.self_id, + }), ) except Exception as e: - logger.opt(colors=True, exception=e).exception( - 'Failed to handle message ' - f'{escape_tag(str(message))}: ') + Log.error(f'Failed to handle message: {message}', e) @overrides(BaseBot) async def call_api(self, api: str, **data) -> NoReturn: @@ -262,10 +253,8 @@ async def send(self, * ``message: Union[MessageChain, MessageSegment, str]``: 要发送的消息 * ``at_sender: bool``: 是否 @ 事件主体 """ - if isinstance(message, MessageSegment): + if not isinstance(message, MessageChain): message = MessageChain(message) - elif isinstance(message, str): - message = MessageChain(MessageSegment.plain(message)) if isinstance(event, FriendMessage): return await self.send_friend_message(target=event.sender.id, message_chain=message) diff --git a/nonebot/adapters/mirai/event/__init__.py b/nonebot/adapters/mirai/event/__init__.py index 1cf92096f3d9..91f4b1273e9f 100644 --- a/nonebot/adapters/mirai/event/__init__.py +++ b/nonebot/adapters/mirai/event/__init__.py @@ -13,7 +13,7 @@ __all__ = [ 'Event', 'GroupChatInfo', 'GroupInfo', 'PrivateChatInfo', 'UserPermission', - 'MessageChain', 'MessageEvent', 'GroupMessage', 'FriendMessage', + 'MessageSource', 'MessageEvent', 'GroupMessage', 'FriendMessage', 'TempMessage', 'NoticeEvent', 'MuteEvent', 'BotMuteEvent', 'BotUnmuteEvent', 'MemberMuteEvent', 'MemberUnmuteEvent', 'BotJoinGroupEvent', 'BotLeaveEventActive', 'BotLeaveEventKick', 'MemberJoinEvent', diff --git a/nonebot/adapters/mirai/event/message.py b/nonebot/adapters/mirai/event/message.py index 26d534d41e84..5dda0857bb76 100644 --- a/nonebot/adapters/mirai/event/message.py +++ b/nonebot/adapters/mirai/event/message.py @@ -1,6 +1,7 @@ -from typing import Any +from datetime import datetime +from typing import Any, Optional -from pydantic import Field +from pydantic import BaseModel, Field from nonebot.typing import overrides @@ -8,9 +9,15 @@ from .base import Event, GroupChatInfo, PrivateChatInfo +class MessageSource(BaseModel): + id: int + time: datetime + + class MessageEvent(Event): """消息事件基类""" message_chain: MessageChain = Field(alias='messageChain') + source: Optional[MessageSource] = None sender: Any @overrides(Event) diff --git a/nonebot/adapters/mirai/message.py b/nonebot/adapters/mirai/message.py index 26fb198c2244..645c919f77c7 100644 --- a/nonebot/adapters/mirai/message.py +++ b/nonebot/adapters/mirai/message.py @@ -44,8 +44,9 @@ def __init__(self, type: MessageType, **data): @overrides(BaseMessageSegment) def __str__(self) -> str: - if self.is_text(): - return self.data.get('text', '') + return self.data['text'] if self.is_text() else repr(self) + + def __repr__(self) -> str: return '[mirai:%s]' % ','.join([ self.type.value, *map( @@ -267,18 +268,20 @@ def poke(cls, name: str): class MessageChain(BaseMessage): """ - Mirai 协议 Messaqge 适配 + Mirai 协议 Message 适配 由于Mirai协议的Message实现较为特殊, 故使用MessageChain命名 """ @overrides(BaseMessage) - def __init__(self, message: Union[List[Dict[str, Any]], - Iterable[MessageSegment], MessageSegment], - **kwargs): + def __init__(self, message: Union[List[Dict[str, + Any]], Iterable[MessageSegment], + MessageSegment, str], **kwargs): super().__init__(**kwargs) if isinstance(message, MessageSegment): self.append(message) + elif isinstance(message, str): + self.append(MessageSegment.plain(text=message)) elif isinstance(message, Iterable): self.extend(self._construct(message)) else: @@ -286,6 +289,19 @@ def __init__(self, message: Union[List[Dict[str, Any]], f'Type {type(message).__name__} is not supported in mirai adapter.' ) + @overrides(BaseMessage) + def reduce(self): + """ + :说明: + + 忽略为空的消息段, 合并相邻的纯文本消息段 + """ + for index, segment in enumerate(self): + segment: MessageSegment + if segment.is_text() and not str(segment).strip(): + self.pop(index) + super().reduce() + @overrides(BaseMessage) def _construct( self, message: Union[List[Dict[str, Any]], Iterable[MessageSegment]] @@ -306,5 +322,22 @@ def export(self) -> List[Dict[str, Any]]: *map(lambda segment: segment.as_dict(), self.copy()) # type: ignore ] + def extract_first(self, *type: MessageType) -> Optional[MessageSegment]: + """ + :说明: + + 弹出该消息链的第一个消息 + + :参数: + + * `*type: MessageType`: 指定的消息类型, 当指定后如类型不匹配不弹出 + """ + if not len(self): + return None + first: MessageSegment = self[0] + if (not type) or (first.type in type): + return self.pop(0) + return None + def __repr__(self) -> str: return f'<{self.__class__.__name__} {[*self.copy()]}>' diff --git a/nonebot/adapters/mirai/utils.py b/nonebot/adapters/mirai/utils.py index cb2b5e2d1caf..74ad9f6e6e39 100644 --- a/nonebot/adapters/mirai/utils.py +++ b/nonebot/adapters/mirai/utils.py @@ -7,10 +7,11 @@ import nonebot.exception as exception from nonebot.log import logger +from nonebot.message import handle_event from nonebot.utils import escape_tag, logger_wrapper -from .event import Event, GroupMessage -from .message import MessageSegment, MessageType +from .event import Event, GroupMessage, MessageEvent, MessageSource +from .message import MessageType if TYPE_CHECKING: from .bot import Bot @@ -20,23 +21,28 @@ class Log: - _log = logger_wrapper('MIRAI') + + @staticmethod + def log(level: str, message: str, exception: Optional[Exception] = None): + logger = logger_wrapper('MIRAI') + message = '' + escape_tag(message) + '' + logger(level=level.upper(), message=message, exception=exception) @classmethod def info(cls, message: Any): - cls._log('INFO', str(message)) + cls.log('INFO', str(message)) @classmethod def debug(cls, message: Any): - cls._log('DEBUG', str(message)) + cls.log('DEBUG', str(message)) @classmethod def warn(cls, message: Any): - cls._log('WARNING', str(message)) + cls.log('WARNING', str(message)) @classmethod def error(cls, message: Any, exception: Optional[Exception] = None): - cls._log('ERROR', str(message), exception=exception) + cls.log('ERROR', str(message), exception=exception) class ActionFailed(exception.ActionFailed): @@ -118,39 +124,55 @@ def wrapper(*args, **kwargs): return wrapper # type: ignore -async def check_tome(bot: "Bot", event: "Event") -> "Event": - if not isinstance(event, GroupMessage): - return event - - def _is_at(event: GroupMessage) -> bool: - for segment in event.message_chain: - segment: MessageSegment - if segment.type != MessageType.AT: - continue - if segment.data['target'] == event.self_id: - return True - return False - - def _is_nick(event: GroupMessage) -> bool: - text = event.get_plaintext() - if not text: - return False - nick_regex = '|'.join( - {i.strip() for i in bot.config.nickname if i.strip()}) +def process_source(bot: "Bot", event: MessageEvent) -> MessageEvent: + source = event.message_chain.extract_first(MessageType.SOURCE) + if source is not None: + event.source = MessageSource.parse_obj(source.data) + return event + + +def process_at(bot: "Bot", event: GroupMessage) -> GroupMessage: + at = event.message_chain.extract_first(MessageType.AT) + if at is not None: + if at.data['target'] == event.self_id: + event.to_me = True + else: + event.message_chain.insert(0, at) + return event + + +def process_nick(bot: "Bot", event: GroupMessage) -> GroupMessage: + plain = event.message_chain.extract_first(MessageType.PLAIN) + if plain is not None: + text = str(plain) + nick_regex = '|'.join(filter(lambda x: x, bot.config.nickname)) matched = re.search(rf"^({nick_regex})([\s,,]*|$)", text, re.IGNORECASE) - if matched is None: - return False - Log.info(f'User is calling me {matched.group(1)}') - return True - - def _is_reply(event: GroupMessage) -> bool: - for segment in event.message_chain: - segment: MessageSegment - if segment.type != MessageType.QUOTE: - continue - if segment.data['senderId'] == event.self_id: - return True - return False - - event.to_me = any([_is_at(event), _is_reply(event), _is_nick(event)]) + if matched is not None: + event.to_me = True + nickname = matched.group(1) + Log.info(f'User is calling me {nickname}') + plain.data['text'] = text[matched.end():] + event.message_chain.insert(0, plain) + return event + + +def process_reply(bot: "Bot", event: GroupMessage) -> GroupMessage: + reply = event.message_chain.extract_first(MessageType.QUOTE) + if reply is not None: + if reply.data['senderId'] == event.self_id: + event.to_me = True + else: + event.message_chain.insert(0, reply) return event + + +async def process_event(bot: "Bot", event: Event) -> None: + if isinstance(event, MessageEvent): + event.message_chain.reduce() + Log.debug(event.message_chain) + event = process_source(bot, event) + if isinstance(event, GroupMessage): + event = process_nick(bot, event) + event = process_at(bot, event) + event = process_reply(bot, event) + await handle_event(bot, event) \ No newline at end of file diff --git a/nonebot/drivers/quart.py b/nonebot/drivers/quart.py new file mode 100644 index 000000000000..9d189d64e54c --- /dev/null +++ b/nonebot/drivers/quart.py @@ -0,0 +1,240 @@ +""" +Quart 驱动适配 +================ + +后端使用方法请参考: `Quart 文档`_ + +.. _Quart 文档: + https://pgjones.gitlab.io/quart/index.html +""" + +import asyncio +from json.decoder import JSONDecodeError +from typing import Any, Callable, Coroutine, Dict, Optional, Type, TypeVar + +import uvicorn + +from nonebot.config import Config as NoneBotConfig +from nonebot.config import Env +from nonebot.drivers import Driver as BaseDriver +from nonebot.drivers import WebSocket as BaseWebSocket +from nonebot.exception import RequestDenied +from nonebot.log import logger +from nonebot.typing import overrides + +try: + from quart import Quart, Request, Response + from quart import Websocket as QuartWebSocket + from quart import exceptions + from quart import request as _request + from quart import websocket as _websocket +except ImportError: + raise ValueError( + 'Please install Quart by using `pip install nonebot2[quart]`') + +_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine]) + + +class Driver(BaseDriver): + """ + Quart 驱动框架 + + :上报地址: + + * ``/{adapter name}/http``: HTTP POST 上报 + * ``/{adapter name}/ws``: WebSocket 上报 + """ + + @overrides(BaseDriver) + def __init__(self, env: Env, config: NoneBotConfig): + super().__init__(env, config) + + self._server_app = Quart(self.__class__.__qualname__) + self._server_app.add_url_rule('//http', + methods=['POST'], + view_func=self._handle_http) + self._server_app.add_websocket('//ws', + view_func=self._handle_ws_reverse) + + @property + @overrides(BaseDriver) + def type(self) -> str: + """驱动名称: ``quart``""" + return 'quart' + + @property + @overrides(BaseDriver) + def server_app(self) -> Quart: + """``Quart`` 对象""" + return self._server_app + + @property + @overrides(BaseDriver) + def asgi(self): + """``Quart`` 对象""" + return self._server_app + + @property + @overrides(BaseDriver) + def logger(self): + """fastapi 使用的 logger""" + return self._server_app.logger + + @overrides(BaseDriver) + def on_startup(self, func: _AsyncCallable) -> _AsyncCallable: + """参考文档: `Startup and Shutdown `_""" + return self.server_app.before_serving(func) # type: ignore + + @overrides(BaseDriver) + def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable: + """参考文档: `Startup and Shutdown `_""" + return self.server_app.after_serving(func) # type: ignore + + @overrides(BaseDriver) + def run(self, + host: Optional[str] = None, + port: Optional[int] = None, + *, + app: Optional[str] = None, + **kwargs): + """使用 ``uvicorn`` 启动 Quart""" + super().run(host, port, app, **kwargs) + LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "default": { + "class": "nonebot.log.LoguruHandler", + }, + }, + "loggers": { + "uvicorn.error": { + "handlers": ["default"], + "level": "INFO" + }, + "uvicorn.access": { + "handlers": ["default"], + "level": "INFO", + }, + }, + } + uvicorn.run(app or self.server_app, + host=host or str(self.config.host), + port=port or self.config.port, + reload=bool(app) and self.config.debug, + debug=self.config.debug, + log_config=LOGGING_CONFIG, + **kwargs) + + @overrides(BaseDriver) + async def _handle_http(self, adapter: str): + request: Request = _request + + try: + data: Dict[str, Any] = await request.get_json() + except Exception as e: + raise exceptions.BadRequest() + + if adapter not in self._adapters: + logger.warning(f'Unknown adapter {adapter}. ' + 'Please register the adapter before use.') + raise exceptions.NotFound() + + BotClass = self._adapters[adapter] + headers = {k: v for k, v in request.headers.items(lower=True)} + + try: + self_id = await BotClass.check_permission(self, 'http', headers, + data) + except RequestDenied as e: + raise exceptions.HTTPException(status_code=e.status_code, + description=e.reason, + name='Request Denied') + if self_id in self._clients: + logger.warning("There's already a reverse websocket connection," + "so the event may be handled twice.") + bot = BotClass('http', self_id) + asyncio.create_task(bot.handle_message(data)) + return Response('', 204) + + @overrides(BaseDriver) + async def _handle_ws_reverse(self, adapter: str): + websocket: QuartWebSocket = _websocket + if adapter not in self._adapters: + logger.warning( + f'Unknown adapter {adapter}. Please register the adapter before use.' + ) + raise exceptions.NotFound() + + BotClass = self._adapters[adapter] + headers = {k: v for k, v in websocket.headers.items(lower=True)} + try: + self_id = await BotClass.check_permission(self, 'websocket', + headers, None) + except RequestDenied as e: + print(e.reason) + raise exceptions.HTTPException(status_code=e.status_code, + description=e.reason, + name='Request Denied') + if self_id in self._clients: + logger.warning("There's already a reverse websocket connection," + "so the event may be handled twice.") + ws = WebSocket(websocket) + bot = BotClass('websocket', self_id, websocket=ws) + await ws.accept() + logger.opt(colors=True).info( + f"WebSocket Connection from {adapter.upper()} " + f"Bot {self_id} Accepted!") + self._bot_connect(bot) + + try: + while not ws.closed: + data = await ws.receive() + if data is None: + continue + asyncio.create_task(bot.handle_message(data)) + finally: + self._bot_disconnect(bot) + + +class WebSocket(BaseWebSocket): + + @overrides(BaseWebSocket) + def __init__(self, websocket: QuartWebSocket): + super().__init__(websocket) + self._closed = False + + @property + @overrides(BaseWebSocket) + def websocket(self) -> QuartWebSocket: + return self._websocket + + @property + @overrides(BaseWebSocket) + def closed(self): + return self._closed + + @overrides(BaseWebSocket) + async def accept(self): + await self.websocket.accept() + self._closed = False + + @overrides(BaseWebSocket) + async def close(self): + self._closed = True + + @overrides(BaseWebSocket) + async def receive(self) -> Optional[Dict[str, Any]]: + data: Optional[Dict[str, Any]] = None + try: + data = await self.websocket.receive_json() + except JSONDecodeError: + logger.warning('Received an invalid json message.') + except asyncio.CancelledError: + self._closed = True + logger.warning('WebSocket disconnected by peer.') + return data + + @overrides(BaseWebSocket) + async def send(self, data: dict): + await self.websocket.send_json(data) diff --git a/nonebot/message.py b/nonebot/message.py index fb860a96b583..cc2268753fd9 100644 --- a/nonebot/message.py +++ b/nonebot/message.py @@ -7,7 +7,7 @@ import asyncio from datetime import datetime -from typing import Set, Type, Optional, Iterable, TYPE_CHECKING +from typing import Set, Type, TYPE_CHECKING from nonebot.log import logger from nonebot.rule import TrieRule diff --git a/nonebot/plugin.py b/nonebot/plugin.py index 3270fd126e5a..e66672cde89c 100644 --- a/nonebot/plugin.py +++ b/nonebot/plugin.py @@ -22,7 +22,7 @@ from nonebot.rule import Rule, startswith, endswith, keyword, command, shell_command, ArgumentParser, regex if TYPE_CHECKING: - from nonebot.adapters import Bot, Event + from nonebot.adapters import Bot, Event, MessageSegment plugins: Dict[str, "Plugin"] = {} """ diff --git a/nonebot/rule.py b/nonebot/rule.py index d9f75a24a584..750223354b4a 100644 --- a/nonebot/rule.py +++ b/nonebot/rule.py @@ -25,7 +25,7 @@ from nonebot.typing import T_State, T_RuleChecker if TYPE_CHECKING: - from nonebot.adapters import Bot, Event + from nonebot.adapters import Bot, Event, MessageSegment class Rule: diff --git a/poetry.lock b/poetry.lock index b89548a3549f..6d41f5cb2333 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[package]] +name = "aiofiles" +version = "0.6.0" +description = "File support for asyncio." +category = "main" +optional = true +python-versions = "*" + [[package]] name = "alabaster" version = "0.7.12" @@ -17,6 +25,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pytz = ">=2015.7" +[[package]] +name = "blinker" +version = "1.4" +description = "Fast, simple object-to-object and broadcast signaling" +category = "main" +optional = true +python-versions = "*" + [[package]] name = "certifi" version = "2020.12.5" @@ -25,14 +41,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "click" version = "7.1.2" @@ -83,6 +91,26 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "h2" +version = "4.0.0" +description = "HTTP/2 State-Machine based protocol implementation" +category = "main" +optional = true +python-versions = ">=3.6.1" + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +category = "main" +optional = true +python-versions = ">=3.6.1" + [[package]] name = "html2text" version = "2020.1.16" @@ -135,13 +163,43 @@ sniffio = "*" brotli = ["brotlipy (>=0.7.0,<0.8.0)"] http2 = ["h2 (>=3.0.0,<4.0.0)"] +[[package]] +name = "hypercorn" +version = "0.11.2" +description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn." +category = "main" +optional = true +python-versions = ">=3.7" + +[package.dependencies] +h11 = "*" +h2 = ">=3.1.0" +priority = "*" +toml = "*" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} +wsproto = ">=0.14.0" + +[package.extras] +h3 = ["aioquic (>=0.9.0,<1.0)"] +tests = ["hypothesis", "mock", "pytest", "pytest-asyncio", "pytest-cov", "pytest-trio", "trio"] +trio = ["trio (>=0.11.0)"] +uvloop = ["uvloop"] + +[[package]] +name = "hyperframe" +version = "6.0.0" +description = "HTTP/2 framing layer for Python" +category = "main" +optional = true +python-versions = ">=3.6.1" + [[package]] name = "idna" -version = "2.10" +version = "3.1" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.4" [[package]] name = "imagesize" @@ -151,11 +209,19 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "itsdangerous" +version = "1.1.0" +description = "Various helpers to pass data to untrusted environments and back." +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "jinja2" version = "2.11.3" description = "A very fast and expressive template engine." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -184,7 +250,7 @@ dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "tox (>=3 name = "markupsafe" version = "1.1.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" @@ -199,6 +265,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" +[[package]] +name = "priority" +version = "1.3.0" +description = "A pure-Python implementation of the HTTP/2 priority tree" +category = "main" +optional = true +python-versions = "*" + [[package]] name = "pydantic" version = "1.7.3" @@ -270,22 +344,38 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "quart" +version = "0.14.1" +description = "A Python ASGI web microframework with the same API as Flask" +category = "main" +optional = true +python-versions = ">=3.7.0" + +[package.dependencies] +aiofiles = "*" +blinker = "*" +click = "*" +hypercorn = ">=0.7.0" +itsdangerous = "*" +jinja2 = "*" +toml = "*" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} +werkzeug = ">=1.0.0" + +[package.extras] +dotenv = ["python-dotenv"] + [[package]] name = "requests" -version = "2.25.1" +version = "2.15.1" description = "Python HTTP for Humans." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.27" +python-versions = "*" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +security = ["cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] [[package]] @@ -453,6 +543,14 @@ python-versions = ">=3.6" [package.extras] full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = true +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "typing-extensions" version = "3.7.4.3" @@ -480,19 +578,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "urllib3" -version = "1.26.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - [[package]] name = "uvicorn" version = "0.11.8" @@ -527,6 +612,18 @@ category = "main" optional = false python-versions = ">=3.6.1" +[[package]] +name = "werkzeug" +version = "1.0.1" +description = "The comprehensive WSGI web application library." +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] +watchdog = ["watchdog"] + [[package]] name = "win32-setctime" version = "1.0.3" @@ -538,6 +635,17 @@ python-versions = ">=3.5" [package.extras] dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] +[[package]] +name = "wsproto" +version = "1.0.0" +description = "WebSockets state-machine based protocol implementation" +category = "main" +optional = true +python-versions = ">=3.6.1" + +[package.dependencies] +h11 = ">=0.9.0,<1" + [[package]] name = "yapf" version = "0.30.0" @@ -546,12 +654,20 @@ category = "dev" optional = false python-versions = "*" +[extras] +all = ["Quart"] +quart = ["Quart"] + [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "9aa4fde8078788e6a12866ba4eb5d17ec6237355c663d6ea74040b6e165cdcf1" +content-hash = "11273401518ba0c93c5e381c6f0c1be02d60106bcda715c7ee7a06a78a8871d5" [metadata.files] +aiofiles = [ + {file = "aiofiles-0.6.0-py3-none-any.whl", hash = "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27"}, + {file = "aiofiles-0.6.0.tar.gz", hash = "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"}, +] alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, @@ -560,14 +676,13 @@ babel = [ {file = "Babel-2.9.0-py2.py3-none-any.whl", hash = "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5"}, {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"}, ] +blinker = [ + {file = "blinker-1.4.tar.gz", hash = "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"}, +] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, ] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, -] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, @@ -588,6 +703,14 @@ h11 = [ {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, ] +h2 = [ + {file = "h2-4.0.0-py3-none-any.whl", hash = "sha256:ac9e293a1990b339d5d71b19c5fe630e3dd4d768c620d1730d355485323f1b25"}, + {file = "h2-4.0.0.tar.gz", hash = "sha256:bb7ac7099dd67a857ed52c815a6192b6b1f5ba6b516237fc24a085341340593d"}, +] +hpack = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] html2text = [ {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"}, {file = "html2text-2020.1.16.tar.gz", hash = "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"}, @@ -614,14 +737,26 @@ httpx = [ {file = "httpx-0.16.1-py3-none-any.whl", hash = "sha256:9cffb8ba31fac6536f2c8cde30df859013f59e4bcc5b8d43901cb3654a8e0a5b"}, {file = "httpx-0.16.1.tar.gz", hash = "sha256:126424c279c842738805974687e0518a94c7ae8d140cd65b9c4f77ac46ffa537"}, ] +hypercorn = [ + {file = "Hypercorn-0.11.2-py3-none-any.whl", hash = "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821"}, + {file = "Hypercorn-0.11.2.tar.gz", hash = "sha256:5ba1e719c521080abd698ff5781a2331e34ef50fc1c89a50960538115a896a9a"}, +] +hyperframe = [ + {file = "hyperframe-6.0.0-py3-none-any.whl", hash = "sha256:a51026b1591cac726fc3d0b7994fbc7dc5efab861ef38503face2930fd7b2d34"}, + {file = "hyperframe-6.0.0.tar.gz", hash = "sha256:742d2a4bc3152a340a49d59f32e33ec420aa8e7054c1444ef5c7efff255842f1"}, +] idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"}, + {file = "idna-3.1.tar.gz", hash = "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"}, ] imagesize = [ {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] +itsdangerous = [ + {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, + {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, +] jinja2 = [ {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, @@ -649,45 +784,30 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] packaging = [ {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] +priority = [ + {file = "priority-1.3.0-py2.py3-none-any.whl", hash = "sha256:be4fcb94b5e37cdeb40af5533afe6dd603bd665fe9c8b3052610fc1001d5d1eb"}, + {file = "priority-1.3.0.tar.gz", hash = "sha256:6bc1961a6d7fcacbfc337769f1a382c8e746566aaa365e78047abe9f66b2ffbe"}, +] pydantic = [ {file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"}, {file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"}, @@ -735,9 +855,13 @@ pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +quart = [ + {file = "Quart-0.14.1-py3-none-any.whl", hash = "sha256:7b13786e07541cc9ce1466fdc6a6ccd5f36eb39118edd25a42d617593cd17707"}, + {file = "Quart-0.14.1.tar.gz", hash = "sha256:429c5b4ff27e1d2f9ca0aacc38f6aba0ff49b38b815448bf24b613d3de12ea02"}, +] requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, + {file = "requests-2.15.1-py2.py3-none-any.whl", hash = "sha256:ff753b2196cd18b1bbeddc9dcd5c864056599f7a7d9a4fb5677e723efa2b7fb9"}, + {file = "requests-2.15.1.tar.gz", hash = "sha256:e5659b9315a0610505e050bb7190bf6fa2ccee1ac295f2b760ef9d8a03ebbb2e"}, ] rfc3986 = [ {file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"}, @@ -784,6 +908,10 @@ starlette = [ {file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"}, {file = "starlette-0.13.6.tar.gz", hash = "sha256:ebe8ee08d9be96a3c9f31b2cb2a24dbdf845247b745664bd8a3f9bd0c977fdbc"}, ] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] typing-extensions = [ {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, @@ -795,10 +923,6 @@ unify = [ untokenize = [ {file = "untokenize-0.1.1.tar.gz", hash = "sha256:3865dbbbb8efb4bb5eaa72f1be7f3e0be00ea8b7f125c69cbd1f5fda926f37a2"}, ] -urllib3 = [ - {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, - {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, -] uvicorn = [ {file = "uvicorn-0.11.8-py3-none-any.whl", hash = "sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef"}, {file = "uvicorn-0.11.8.tar.gz", hash = "sha256:46a83e371f37ea7ff29577d00015f02c942410288fb57def6440f2653fff1d26"}, @@ -838,10 +962,18 @@ websockets = [ {file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"}, {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, ] +werkzeug = [ + {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, + {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, +] win32-setctime = [ {file = "win32_setctime-1.0.3-py3-none-any.whl", hash = "sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e"}, {file = "win32_setctime-1.0.3.tar.gz", hash = "sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b"}, ] +wsproto = [ + {file = "wsproto-1.0.0-py3-none-any.whl", hash = "sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f"}, + {file = "wsproto-1.0.0.tar.gz", hash = "sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38"}, +] yapf = [ {file = "yapf-0.30.0-py2.py3-none-any.whl", hash = "sha256:3abf61ba67cf603069710d30acbc88cfe565d907e16ad81429ae90ce9651e0c9"}, {file = "yapf-0.30.0.tar.gz", hash = "sha256:3000abee4c28daebad55da6c85f3cd07b8062ce48e2e9943c8da1b9667d48427"}, diff --git a/pyproject.toml b/pyproject.toml index ee03af337c00..58566a6d0a98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nonebot2" -version = "2.0.0a9.post1" +version = "2.0.0a10" description = "An asynchronous python bot framework." authors = ["yanyongyu "] license = "MIT" @@ -31,12 +31,17 @@ fastapi = "^0.63.0" uvicorn = "^0.11.5" websockets = "^8.1" pydantic = {extras = ["dotenv", "typing_extensions"], version = "^1.7.3"} +Quart = {version = "^0.14.1", optional = true} [tool.poetry.dev-dependencies] yapf = "^0.30.0" sphinx = "^3.4.1" sphinx-markdown-builder = { git = "https://github.com/nonebot/sphinx-markdown-builder.git" } +[tool.poetry.extras] +quart = ["quart"] +all = ["quart"] + # [[tool.poetry.source]] # name = "aliyun" # url = "https://mirrors.aliyun.com/pypi/simple/" diff --git a/tests/.env.dev b/tests/.env.dev index 33e6f835ed29..0d7d878f1027 100644 --- a/tests/.env.dev +++ b/tests/.env.dev @@ -14,4 +14,4 @@ CUSTOM_CONFIG3= MIRAI_AUTH_KEY=12345678 MIRAI_HOST=127.0.0.1 -MIRAI_PORT=8080 \ No newline at end of file +MIRAI_PORT=8080 diff --git a/tests/test_plugins/test_mirai.py b/tests/test_plugins/test_mirai.py index a5da93aef77b..c518290aa179 100644 --- a/tests/test_plugins/test_mirai.py +++ b/tests/test_plugins/test_mirai.py @@ -1,13 +1,20 @@ -from nonebot.plugin import on_message +from nonebot.plugin import on_keyword, on_command +from nonebot.rule import to_me from nonebot.adapters.mirai import Bot, MessageEvent -message_test = on_message() +message_test = on_keyword({'reply'}, rule=to_me()) @message_test.handle() async def _message(bot: Bot, event: MessageEvent): text = event.get_plaintext() - if not text: - return - reversed_text = ''.join(reversed(text)) - await bot.send(event, reversed_text, at_sender=True) + await bot.send(event, text, at_sender=True) + + +command_test = on_command('miecho') + + +@command_test.handle() +async def _echo(bot: Bot, event: MessageEvent): + text = event.get_plaintext() + await bot.send(event, text, at_sender=True) \ No newline at end of file