diff --git a/README.md b/README.md index a265fe8..48ff474 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![Python Version](https://img.shields.io/badge/python-3.7+-blue.svg) ![OneBot Version](https://img.shields.io/badge/OneBot-v10,v11-black.svg) -**aiocqhttp** 是 [OneBot](https://github.com/howmanybots/onebot) (原 [酷Q](https://cqp.cc) 的 [CQHTTP 插件](https://cqhttp.cc)) 的 Python SDK,采用异步 I/O,封装了 web 服务器相关的代码,支持 OneBot 的 HTTP 和反向 WebSocket 两种通信方式,让使用 Python 的开发者能方便地开发插件。 +**aiocqhttp** 是 [OneBot](https://github.com/howmanybots/onebot) (原 [酷Q](https://cqp.cc) 的 [CQHTTP 插件](https://cqhttp.cc)) 的 Python SDK,采用异步 I/O,封装了 web 服务器相关的代码,支持 OneBot 的 HTTP、正向 WebSocket 和反向 WebSocket 三种通信方式,让使用 Python 的开发者能方便地开发插件。 本 SDK 要求使用 Python 3.7 或更高版本,以及建议搭配支持 OneBot v11 的 OneBot 实现。 diff --git a/aiocqhttp/__init__.py b/aiocqhttp/__init__.py index 97d7292..936ea05 100644 --- a/aiocqhttp/__init__.py +++ b/aiocqhttp/__init__.py @@ -19,8 +19,8 @@ from quart import Quart, request, abort, jsonify, websocket, Response from .api import AsyncApi, SyncApi -from .api_impl import (SyncWrapperApi, HttpApi, WebSocketReverseApi, - UnifiedApi, ResultStore) +from .api_impl import (SyncWrapperApi, HttpApi, WebSocketReverseApi, UnifiedApi, + ResultStore, WebSocketForwardApi, _is_websocket_url) from .bus import EventBus from .exceptions import Error, TimingError from .event import Event @@ -105,8 +105,10 @@ def __init__(self, ``import_name`` 参数为当前模块(使用 `CQHttp` 的模块)的导入名,通常传入 ``__name__`` 或不传入。 - ``api_root`` 参数为 OneBot API 的 URL,``access_token`` 和 - ``secret`` 参数为 OneBot 配置中填写的对应项。 + ``api_root`` 参数为 OneBot API 的地址,支持 HTTP URL(如 + ``http://127.0.0.1:5700``)或正向 WebSocket URL(如 + ``ws://127.0.0.1:6700/``)。``access_token`` 和 ``secret`` 参数为 OneBot + 配置中填写的对应项。 ``message_class`` 参数为要用来对 `Event.message` 进行转换的消息类,可使用 `Message`,例如: @@ -155,12 +157,35 @@ def _configure(self, api_timeout_sec = api_timeout_sec or 60 # wait for 60 secs by default self._access_token = access_token self._secret = secret - self._api._http_api = HttpApi(api_root, access_token, api_timeout_sec) + + # Configure API implementations based on api_root type + http_api = None + wsf_api = None + + if _is_websocket_url(api_root): + # Forward WebSocket mode + try: + wsf_api = WebSocketForwardApi(ws_url=api_root, + access_token=access_token, + timeout_sec=api_timeout_sec, + event_handler=self._handle_event) + except ImportError as e: + self.logger.error(f"Failed to create WebSocketForwardApi: {e}") + raise + else: + # HTTP mode + http_api = HttpApi(api_root, access_token, api_timeout_sec) + + # Always configure reverse WebSocket (independent) self._wsr_api_clients = {} # connected wsr api clients self._wsr_event_clients = set() - self._api._wsr_api = WebSocketReverseApi(self._wsr_api_clients, - self._wsr_event_clients, - api_timeout_sec) + wsr_api = WebSocketReverseApi(self._wsr_api_clients, + self._wsr_event_clients, api_timeout_sec) + + # Update the existing UnifiedApi instance instead of creating a new one + self._api._http_api = http_api + self._api._wsr_api = wsr_api + self._api._wsf_api = wsf_api async def _before_serving(self): self._loop = asyncio.get_running_loop() diff --git a/aiocqhttp/api.pyi b/aiocqhttp/api.pyi index ef54831..fdd86e3 100644 --- a/aiocqhttp/api.pyi +++ b/aiocqhttp/api.pyi @@ -3,9 +3,9 @@ from typing import Union, Awaitable, Any, Callable, Optional, Dict, List from aiocqhttp.typing import Message_T - if sys.version_info >= (3, 8, 0): from typing import TypedDict + class _send_private_msg_ret(TypedDict): message_id: int @@ -149,23 +149,27 @@ else: class Api: + def call_action( - self, - action: str, - **params, - ) -> Union[Awaitable[Any], Any]: ... + self, + action: str, + **params, + ) -> Union[Awaitable[Any], Any]: + ... def __getattr__( - self, - item: str, - ) -> Callable[..., Union[Awaitable[Any], Any]]: ... + self, + item: str, + ) -> Callable[..., Union[Awaitable[Any], Any]]: + ... def send_private_msg( - self, *, - user_id: int, - message: Message_T, - auto_escape: bool = False, - self_id: Optional[int] = None, + self, + *, + user_id: int, + message: Message_T, + auto_escape: bool = False, + self_id: Optional[int] = None, ) -> Union[Awaitable[_send_private_msg_ret], _send_private_msg_ret]: """ 发送私聊消息。 @@ -178,11 +182,12 @@ class Api: """ def send_group_msg( - self, *, - group_id: int, - message: Message_T, - auto_escape: bool = False, - self_id: Optional[int] = None, + self, + *, + group_id: int, + message: Message_T, + auto_escape: bool = False, + self_id: Optional[int] = None, ) -> Union[Awaitable[_send_group_msg_ret], _send_group_msg_ret]: """ 发送群消息。 @@ -195,13 +200,14 @@ class Api: """ def send_msg( - self, *, - message_type: Optional[str] = None, - user_id: Optional[int] = None, - group_id: Optional[int] = None, - message: Message_T, - auto_escape: bool = False, - self_id: Optional[int] = None, + self, + *, + message_type: Optional[str] = None, + user_id: Optional[int] = None, + group_id: Optional[int] = None, + message: Message_T, + auto_escape: bool = False, + self_id: Optional[int] = None, ) -> Union[Awaitable[_send_msg_ret], _send_msg_ret]: """ 发送消息。 @@ -216,9 +222,10 @@ class Api: """ def delete_msg( - self, *, - message_id: int, - self_id: Optional[int] = None, + self, + *, + message_id: int, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 撤回消息。 @@ -229,9 +236,10 @@ class Api: """ def get_msg( - self, *, - message_id: int, - self_id: Optional[int] = None, + self, + *, + message_id: int, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_msg_ret], _get_msg_ret]: """ 获取消息。 @@ -242,9 +250,10 @@ class Api: """ def get_forward_msg( - self, *, - id: str, - self_id: Optional[int] = None, + self, + *, + id: str, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_forward_msg_ret], _get_forward_msg_ret]: """ 获取合并转发消息。 @@ -255,10 +264,11 @@ class Api: """ def send_like( - self, *, - user_id: int, - times: int = 1, - self_id: Optional[int] = None, + self, + *, + user_id: int, + times: int = 1, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 发送好友赞。 @@ -270,11 +280,12 @@ class Api: """ def set_group_kick( - self, *, - group_id: int, - user_id: int, - reject_add_request: bool = False, - self_id: Optional[int] = None, + self, + *, + group_id: int, + user_id: int, + reject_add_request: bool = False, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 群组踢人。 @@ -287,11 +298,12 @@ class Api: """ def set_group_ban( - self, *, - group_id: int, - user_id: int, - duration: int = 30 * 60, - self_id: Optional[int] = None, + self, + *, + group_id: int, + user_id: int, + duration: int = 30 * 60, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 群组单人禁言。 @@ -304,12 +316,13 @@ class Api: """ def set_group_anonymous_ban( - self, *, - group_id: int, - anonymous: Optional[Dict[str, Any]] = None, - anonymous_flag: Optional[str] = None, - duration: int = 30 * 60, - self_id: Optional[int] = None, + self, + *, + group_id: int, + anonymous: Optional[Dict[str, Any]] = None, + anonymous_flag: Optional[str] = None, + duration: int = 30 * 60, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 群组匿名用户禁言。 @@ -323,10 +336,11 @@ class Api: """ def set_group_whole_ban( - self, *, - group_id: int, - enable: bool = True, - self_id: Optional[int] = None, + self, + *, + group_id: int, + enable: bool = True, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 群组全员禁言。 @@ -338,11 +352,12 @@ class Api: """ def set_group_admin( - self, *, - group_id: int, - user_id: int, - enable: bool = True, - self_id: Optional[int] = None, + self, + *, + group_id: int, + user_id: int, + enable: bool = True, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 群组设置管理员。 @@ -355,10 +370,11 @@ class Api: """ def set_group_anonymous( - self, *, - group_id: int, - enable: bool = True, - self_id: Optional[int] = None, + self, + *, + group_id: int, + enable: bool = True, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 群组匿名。 @@ -370,11 +386,12 @@ class Api: """ def set_group_card( - self, *, - group_id: int, - user_id: int, - card: str = '', - self_id: Optional[int] = None, + self, + *, + group_id: int, + user_id: int, + card: str = '', + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 设置群名片(群备注)。 @@ -387,10 +404,11 @@ class Api: """ def set_group_name( - self, *, - group_id: int, - group_name: str, - self_id: Optional[int] = None, + self, + *, + group_id: int, + group_name: str, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 设置群名。 @@ -402,10 +420,11 @@ class Api: """ def set_group_leave( - self, *, - group_id: int, - is_dismiss: bool = False, - self_id: Optional[int] = None, + self, + *, + group_id: int, + is_dismiss: bool = False, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 退出群组。 @@ -417,12 +436,13 @@ class Api: """ def set_group_special_title( - self, *, - group_id: int, - user_id: int, - special_title: str = '', - duration: int = -1, - self_id: Optional[int] = None, + self, + *, + group_id: int, + user_id: int, + special_title: str = '', + duration: int = -1, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 设置群组专属头衔。 @@ -436,11 +456,12 @@ class Api: """ def set_friend_add_request( - self, *, - flag: str, - approve: bool = True, - remark: str = '', - self_id: Optional[int] = None, + self, + *, + flag: str, + approve: bool = True, + remark: str = '', + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 处理加好友请求。 @@ -453,12 +474,13 @@ class Api: """ def set_group_add_request( - self, *, - flag: str, - sub_type: str, - approve: bool = True, - reason: str = '', - self_id: Optional[int] = None, + self, + *, + flag: str, + sub_type: str, + approve: bool = True, + reason: str = '', + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 处理加群请求/邀请。 @@ -472,8 +494,9 @@ class Api: """ def get_login_info( - self, *, - self_id: Optional[int] = None, + self, + *, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_login_info_ret], _get_login_info_ret]: """ 获取登录号信息。 @@ -483,10 +506,11 @@ class Api: """ def get_stranger_info( - self, *, - user_id: int, - no_cache: bool = False, - self_id: Optional[int] = None, + self, + *, + user_id: int, + no_cache: bool = False, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_stranger_info_ret], _get_stranger_info_ret]: """ 获取陌生人信息。 @@ -498,9 +522,11 @@ class Api: """ def get_friend_list( - self, *, - self_id: Optional[int] = None, - ) -> Union[Awaitable[List[_get_friend_list_ret]], List[_get_friend_list_ret]]: + self, + *, + self_id: Optional[int] = None, + ) -> Union[Awaitable[List[_get_friend_list_ret]], + List[_get_friend_list_ret]]: """ 获取好友列表。 @@ -509,10 +535,11 @@ class Api: """ def get_group_info( - self, *, - group_id: int, - no_cache: bool = False, - self_id: Optional[int] = None, + self, + *, + group_id: int, + no_cache: bool = False, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_group_info_ret], _get_group_info_ret]: """ 获取群信息。 @@ -524,8 +551,9 @@ class Api: """ def get_group_list( - self, *, - self_id: Optional[int] = None, + self, + *, + self_id: Optional[int] = None, ) -> Union[Awaitable[List[_get_group_list_ret]], List[_get_group_list_ret]]: """ 获取群列表。 @@ -535,12 +563,14 @@ class Api: """ def get_group_member_info( - self, *, - group_id: int, - user_id: int, - no_cache: bool = False, - self_id: Optional[int] = None, - ) -> Union[Awaitable[_get_group_member_info_ret], _get_group_member_info_ret]: + self, + *, + group_id: int, + user_id: int, + no_cache: bool = False, + self_id: Optional[int] = None, + ) -> Union[Awaitable[_get_group_member_info_ret], + _get_group_member_info_ret]: """ 获取群成员信息。 @@ -552,10 +582,12 @@ class Api: """ def get_group_member_list( - self, *, - group_id: int, - self_id: Optional[int] = None, - ) -> Union[Awaitable[List[_get_group_member_list_ret]], List[_get_group_member_list_ret]]: + self, + *, + group_id: int, + self_id: Optional[int] = None, + ) -> Union[Awaitable[List[_get_group_member_list_ret]], + List[_get_group_member_list_ret]]: """ 获取群成员列表。 @@ -565,10 +597,11 @@ class Api: """ def get_group_honor_info( - self, *, - group_id: int, - type: str, - self_id: Optional[int] = None, + self, + *, + group_id: int, + type: str, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_group_honor_info_ret], _get_group_honor_info_ret]: """ 获取群荣誉信息。 @@ -580,9 +613,10 @@ class Api: """ def get_cookies( - self, *, - domain: str = '', - self_id: Optional[int] = None, + self, + *, + domain: str = '', + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_cookies_ret], _get_cookies_ret]: """ 获取 Cookies。 @@ -593,8 +627,9 @@ class Api: """ def get_csrf_token( - self, *, - self_id: Optional[int] = None, + self, + *, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_csrf_token_ret], _get_csrf_token_ret]: """ 获取 CSRF Token。 @@ -604,9 +639,10 @@ class Api: """ def get_credentials( - self, *, - domain: str = '', - self_id: Optional[int] = None, + self, + *, + domain: str = '', + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_credentials_ret], _get_credentials_ret]: """ 获取 QQ 相关接口凭证。 @@ -617,10 +653,11 @@ class Api: """ def get_record( - self, *, - file: str, - out_format: str, - self_id: Optional[int] = None, + self, + *, + file: str, + out_format: str, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_record_ret], _get_record_ret]: """ 获取语音。 @@ -632,9 +669,10 @@ class Api: """ def get_image( - self, *, - file: str, - self_id: Optional[int] = None, + self, + *, + file: str, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_image_ret], _get_image_ret]: """ 获取图片。 @@ -645,8 +683,9 @@ class Api: """ def can_send_image( - self, *, - self_id: Optional[int] = None, + self, + *, + self_id: Optional[int] = None, ) -> Union[Awaitable[_can_send_image_ret], _can_send_image_ret]: """ 检查是否可以发送图片。 @@ -656,8 +695,9 @@ class Api: """ def can_send_record( - self, *, - self_id: Optional[int] = None, + self, + *, + self_id: Optional[int] = None, ) -> Union[Awaitable[_can_send_record_ret], _can_send_record_ret]: """ 检查是否可以发送语音。 @@ -667,8 +707,9 @@ class Api: """ def get_status( - self, *, - self_id: Optional[int] = None, + self, + *, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_status_ret], _get_status_ret]: """ 获取运行状态。 @@ -678,8 +719,9 @@ class Api: """ def get_version_info( - self, *, - self_id: Optional[int] = None, + self, + *, + self_id: Optional[int] = None, ) -> Union[Awaitable[_get_version_info_ret], _get_version_info_ret]: """ 获取版本信息。 @@ -689,9 +731,10 @@ class Api: """ def set_restart( - self, *, - delay: int = 0, - self_id: Optional[int] = None, + self, + *, + delay: int = 0, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 重启 OneBot 实现。 @@ -702,8 +745,9 @@ class Api: """ def clean_cache( - self, *, - self_id: Optional[int] = None, + self, + *, + self_id: Optional[int] = None, ) -> Union[Awaitable[None], None]: """ 清理缓存。 @@ -715,531 +759,687 @@ class Api: # definition to avoid union return types class AsyncApi(Api): + async def call_action( - self, - action: str, - **params, - ) -> Any: ... + self, + action: str, + **params, + ) -> Any: + ... async def send_private_msg( - self, *, - user_id: int, - message: Message_T, - auto_escape: bool = False, - self_id: Optional[int] = None, - ) -> _send_private_msg_ret: ... + self, + *, + user_id: int, + message: Message_T, + auto_escape: bool = False, + self_id: Optional[int] = None, + ) -> _send_private_msg_ret: + ... async def send_group_msg( - self, *, - group_id: int, - message: Message_T, - auto_escape: bool = False, - self_id: Optional[int] = None, - ) -> _send_group_msg_ret: ... + self, + *, + group_id: int, + message: Message_T, + auto_escape: bool = False, + self_id: Optional[int] = None, + ) -> _send_group_msg_ret: + ... async def send_msg( - self, *, - message_type: Optional[str] = None, - user_id: Optional[int] = None, - group_id: Optional[int] = None, - message: Message_T, - auto_escape: bool = False, - self_id: Optional[int] = None, - ) -> _send_msg_ret: ... + self, + *, + message_type: Optional[str] = None, + user_id: Optional[int] = None, + group_id: Optional[int] = None, + message: Message_T, + auto_escape: bool = False, + self_id: Optional[int] = None, + ) -> _send_msg_ret: + ... async def delete_msg( - self, *, - message_id: int, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + message_id: int, + self_id: Optional[int] = None, + ) -> None: + ... async def get_msg( - self, *, - message_id: int, - self_id: Optional[int] = None, - ) -> _get_msg_ret: ... + self, + *, + message_id: int, + self_id: Optional[int] = None, + ) -> _get_msg_ret: + ... async def get_forward_msg( - self, *, - id: str, - self_id: Optional[int] = None, - ) -> _get_forward_msg_ret: ... + self, + *, + id: str, + self_id: Optional[int] = None, + ) -> _get_forward_msg_ret: + ... async def send_like( - self, *, - user_id: int, - times: int = 1, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + user_id: int, + times: int = 1, + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_kick( - self, *, - group_id: int, - user_id: int, - reject_add_request: bool = False, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + user_id: int, + reject_add_request: bool = False, + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_ban( - self, *, - group_id: int, - user_id: int, - duration: int = 30 * 60, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + user_id: int, + duration: int = 30 * 60, + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_anonymous_ban( - self, *, - group_id: int, - anonymous: Optional[Dict[str, Any]] = None, - anonymous_flag: Optional[str] = None, - duration: int = 30 * 60, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + anonymous: Optional[Dict[str, Any]] = None, + anonymous_flag: Optional[str] = None, + duration: int = 30 * 60, + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_whole_ban( - self, *, - group_id: int, - enable: bool = True, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + enable: bool = True, + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_admin( - self, *, - group_id: int, - user_id: int, - enable: bool = True, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + user_id: int, + enable: bool = True, + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_anonymous( - self, *, - group_id: int, - enable: bool = True, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + enable: bool = True, + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_card( - self, *, - group_id: int, - user_id: int, - card: str = '', - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + user_id: int, + card: str = '', + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_name( - self, *, - group_id: int, - group_name: str, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + group_name: str, + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_leave( - self, *, - group_id: int, - is_dismiss: bool = False, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + is_dismiss: bool = False, + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_special_title( - self, *, - group_id: int, - user_id: int, - special_title: str = '', - duration: int = -1, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + user_id: int, + special_title: str = '', + duration: int = -1, + self_id: Optional[int] = None, + ) -> None: + ... async def set_friend_add_request( - self, *, - flag: str, - approve: bool = True, - remark: str = '', - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + flag: str, + approve: bool = True, + remark: str = '', + self_id: Optional[int] = None, + ) -> None: + ... async def set_group_add_request( - self, *, - flag: str, - sub_type: str, - approve: bool = True, - reason: str = '', - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + flag: str, + sub_type: str, + approve: bool = True, + reason: str = '', + self_id: Optional[int] = None, + ) -> None: + ... async def get_login_info( - self, *, - self_id: Optional[int] = None, - ) -> _get_login_info_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _get_login_info_ret: + ... async def get_stranger_info( - self, *, - user_id: int, - no_cache: bool = False, - self_id: Optional[int] = None, - ) -> _get_stranger_info_ret: ... + self, + *, + user_id: int, + no_cache: bool = False, + self_id: Optional[int] = None, + ) -> _get_stranger_info_ret: + ... async def get_friend_list( - self, *, - self_id: Optional[int] = None, - ) -> List[_get_friend_list_ret]: ... + self, + *, + self_id: Optional[int] = None, + ) -> List[_get_friend_list_ret]: + ... async def get_group_info( - self, *, - group_id: int, - no_cache: bool = False, - self_id: Optional[int] = None, - ) -> _get_group_info_ret: ... + self, + *, + group_id: int, + no_cache: bool = False, + self_id: Optional[int] = None, + ) -> _get_group_info_ret: + ... async def get_group_list( - self, *, - self_id: Optional[int] = None, - ) -> List[_get_group_list_ret]: ... + self, + *, + self_id: Optional[int] = None, + ) -> List[_get_group_list_ret]: + ... async def get_group_member_info( - self, *, - group_id: int, - user_id: int, - no_cache: bool = False, - self_id: Optional[int] = None, - ) -> _get_group_member_info_ret: ... + self, + *, + group_id: int, + user_id: int, + no_cache: bool = False, + self_id: Optional[int] = None, + ) -> _get_group_member_info_ret: + ... async def get_group_member_list( - self, *, - group_id: int, - self_id: Optional[int] = None, - ) -> List[_get_group_member_list_ret]: ... + self, + *, + group_id: int, + self_id: Optional[int] = None, + ) -> List[_get_group_member_list_ret]: + ... async def get_group_honor_info( - self, *, - group_id: int, - type: str, - self_id: Optional[int] = None, - ) -> _get_group_honor_info_ret: ... + self, + *, + group_id: int, + type: str, + self_id: Optional[int] = None, + ) -> _get_group_honor_info_ret: + ... async def get_cookies( - self, *, - domain: str = '', - self_id: Optional[int] = None, - ) -> _get_cookies_ret: ... + self, + *, + domain: str = '', + self_id: Optional[int] = None, + ) -> _get_cookies_ret: + ... async def get_csrf_token( - self, *, - self_id: Optional[int] = None, - ) -> _get_csrf_token_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _get_csrf_token_ret: + ... async def get_credentials( - self, *, - domain: str = '', - self_id: Optional[int] = None, - ) -> _get_credentials_ret: ... + self, + *, + domain: str = '', + self_id: Optional[int] = None, + ) -> _get_credentials_ret: + ... async def get_record( - self, *, - file: str, - out_format: str, - self_id: Optional[int] = None, - ) -> _get_record_ret: ... + self, + *, + file: str, + out_format: str, + self_id: Optional[int] = None, + ) -> _get_record_ret: + ... async def get_image( - self, *, - file: str, - self_id: Optional[int] = None, - ) -> _get_image_ret: ... + self, + *, + file: str, + self_id: Optional[int] = None, + ) -> _get_image_ret: + ... async def can_send_image( - self, *, - self_id: Optional[int] = None, - ) -> _can_send_image_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _can_send_image_ret: + ... async def can_send_record( - self, *, - self_id: Optional[int] = None, - ) -> _can_send_record_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _can_send_record_ret: + ... async def get_status( - self, *, - self_id: Optional[int] = None, - ) -> _get_status_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _get_status_ret: + ... async def get_version_info( - self, *, - self_id: Optional[int] = None, - ) -> _get_version_info_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _get_version_info_ret: + ... async def set_restart( - self, *, - delay: int = 0, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + delay: int = 0, + self_id: Optional[int] = None, + ) -> None: + ... async def clean_cache( - self, *, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + self_id: Optional[int] = None, + ) -> None: + ... # definition to avoid union return types class SyncApi(Api): + def call_action( - self, - action: str, - **params, - ) -> Any: ... + self, + action: str, + **params, + ) -> Any: + ... def send_private_msg( - self, *, - user_id: int, - message: Message_T, - auto_escape: bool = False, - self_id: Optional[int] = None, - ) -> _send_private_msg_ret: ... + self, + *, + user_id: int, + message: Message_T, + auto_escape: bool = False, + self_id: Optional[int] = None, + ) -> _send_private_msg_ret: + ... def send_group_msg( - self, *, - group_id: int, - message: Message_T, - auto_escape: bool = False, - self_id: Optional[int] = None, - ) -> _send_group_msg_ret: ... + self, + *, + group_id: int, + message: Message_T, + auto_escape: bool = False, + self_id: Optional[int] = None, + ) -> _send_group_msg_ret: + ... def send_msg( - self, *, - message_type: Optional[str] = None, - user_id: Optional[int] = None, - group_id: Optional[int] = None, - message: Message_T, - auto_escape: bool = False, - self_id: Optional[int] = None, - ) -> _send_msg_ret: ... + self, + *, + message_type: Optional[str] = None, + user_id: Optional[int] = None, + group_id: Optional[int] = None, + message: Message_T, + auto_escape: bool = False, + self_id: Optional[int] = None, + ) -> _send_msg_ret: + ... def delete_msg( - self, *, - message_id: int, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + message_id: int, + self_id: Optional[int] = None, + ) -> None: + ... def get_msg( - self, *, - message_id: int, - self_id: Optional[int] = None, - ) -> _get_msg_ret: ... + self, + *, + message_id: int, + self_id: Optional[int] = None, + ) -> _get_msg_ret: + ... def get_forward_msg( - self, *, - id: str, - self_id: Optional[int] = None, - ) -> _get_forward_msg_ret: ... + self, + *, + id: str, + self_id: Optional[int] = None, + ) -> _get_forward_msg_ret: + ... def send_like( - self, *, - user_id: int, - times: int = 1, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + user_id: int, + times: int = 1, + self_id: Optional[int] = None, + ) -> None: + ... def set_group_kick( - self, *, - group_id: int, - user_id: int, - reject_add_request: bool = False, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + user_id: int, + reject_add_request: bool = False, + self_id: Optional[int] = None, + ) -> None: + ... def set_group_ban( - self, *, - group_id: int, - user_id: int, - duration: int = 30 * 60, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + user_id: int, + duration: int = 30 * 60, + self_id: Optional[int] = None, + ) -> None: + ... def set_group_anonymous_ban( - self, *, - group_id: int, - anonymous: Optional[Dict[str, Any]] = None, - anonymous_flag: Optional[str] = None, - duration: int = 30 * 60, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + anonymous: Optional[Dict[str, Any]] = None, + anonymous_flag: Optional[str] = None, + duration: int = 30 * 60, + self_id: Optional[int] = None, + ) -> None: + ... def set_group_whole_ban( - self, *, - group_id: int, - enable: bool = True, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + enable: bool = True, + self_id: Optional[int] = None, + ) -> None: + ... def set_group_admin( - self, *, - group_id: int, - user_id: int, - enable: bool = True, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + user_id: int, + enable: bool = True, + self_id: Optional[int] = None, + ) -> None: + ... def set_group_anonymous( - self, *, - group_id: int, - enable: bool = True, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + enable: bool = True, + self_id: Optional[int] = None, + ) -> None: + ... def set_group_card( - self, *, - group_id: int, - user_id: int, - card: str = '', - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + user_id: int, + card: str = '', + self_id: Optional[int] = None, + ) -> None: + ... def set_group_name( - self, *, - group_id: int, - group_name: str, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + group_name: str, + self_id: Optional[int] = None, + ) -> None: + ... def set_group_leave( - self, *, - group_id: int, - is_dismiss: bool = False, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + is_dismiss: bool = False, + self_id: Optional[int] = None, + ) -> None: + ... def set_group_special_title( - self, *, - group_id: int, - user_id: int, - special_title: str = '', - duration: int = -1, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + group_id: int, + user_id: int, + special_title: str = '', + duration: int = -1, + self_id: Optional[int] = None, + ) -> None: + ... def set_friend_add_request( - self, *, - flag: str, - approve: bool = True, - remark: str = '', - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + flag: str, + approve: bool = True, + remark: str = '', + self_id: Optional[int] = None, + ) -> None: + ... def set_group_add_request( - self, *, - flag: str, - sub_type: str, - approve: bool = True, - reason: str = '', - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + flag: str, + sub_type: str, + approve: bool = True, + reason: str = '', + self_id: Optional[int] = None, + ) -> None: + ... def get_login_info( - self, *, - self_id: Optional[int] = None, - ) -> _get_login_info_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _get_login_info_ret: + ... def get_stranger_info( - self, *, - user_id: int, - no_cache: bool = False, - self_id: Optional[int] = None, - ) -> _get_stranger_info_ret: ... + self, + *, + user_id: int, + no_cache: bool = False, + self_id: Optional[int] = None, + ) -> _get_stranger_info_ret: + ... def get_friend_list( - self, *, - self_id: Optional[int] = None, - ) -> List[_get_friend_list_ret]: ... + self, + *, + self_id: Optional[int] = None, + ) -> List[_get_friend_list_ret]: + ... def get_group_info( - self, *, - group_id: int, - no_cache: bool = False, - self_id: Optional[int] = None, - ) -> _get_group_info_ret: ... + self, + *, + group_id: int, + no_cache: bool = False, + self_id: Optional[int] = None, + ) -> _get_group_info_ret: + ... def get_group_list( - self, *, - self_id: Optional[int] = None, - ) -> List[_get_group_list_ret]: ... + self, + *, + self_id: Optional[int] = None, + ) -> List[_get_group_list_ret]: + ... def get_group_member_info( - self, *, - group_id: int, - user_id: int, - no_cache: bool = False, - self_id: Optional[int] = None, - ) -> _get_group_member_info_ret: ... + self, + *, + group_id: int, + user_id: int, + no_cache: bool = False, + self_id: Optional[int] = None, + ) -> _get_group_member_info_ret: + ... def get_group_member_list( - self, *, - group_id: int, - self_id: Optional[int] = None, - ) -> List[_get_group_member_list_ret]: ... + self, + *, + group_id: int, + self_id: Optional[int] = None, + ) -> List[_get_group_member_list_ret]: + ... def get_group_honor_info( - self, *, - group_id: int, - type: str, - self_id: Optional[int] = None, - ) -> _get_group_honor_info_ret: ... + self, + *, + group_id: int, + type: str, + self_id: Optional[int] = None, + ) -> _get_group_honor_info_ret: + ... def get_cookies( - self, *, - domain: str = '', - self_id: Optional[int] = None, - ) -> _get_cookies_ret: ... + self, + *, + domain: str = '', + self_id: Optional[int] = None, + ) -> _get_cookies_ret: + ... def get_csrf_token( - self, *, - self_id: Optional[int] = None, - ) -> _get_csrf_token_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _get_csrf_token_ret: + ... def get_credentials( - self, *, - domain: str = '', - self_id: Optional[int] = None, - ) -> _get_credentials_ret: ... + self, + *, + domain: str = '', + self_id: Optional[int] = None, + ) -> _get_credentials_ret: + ... def get_record( - self, *, - file: str, - out_format: str, - self_id: Optional[int] = None, - ) -> _get_record_ret: ... + self, + *, + file: str, + out_format: str, + self_id: Optional[int] = None, + ) -> _get_record_ret: + ... def get_image( - self, *, - file: str, - self_id: Optional[int] = None, - ) -> _get_image_ret: ... + self, + *, + file: str, + self_id: Optional[int] = None, + ) -> _get_image_ret: + ... def can_send_image( - self, *, - self_id: Optional[int] = None, - ) -> _can_send_image_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _can_send_image_ret: + ... def can_send_record( - self, *, - self_id: Optional[int] = None, - ) -> _can_send_record_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _can_send_record_ret: + ... def get_status( - self, *, - self_id: Optional[int] = None, - ) -> _get_status_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _get_status_ret: + ... def get_version_info( - self, *, - self_id: Optional[int] = None, - ) -> _get_version_info_ret: ... + self, + *, + self_id: Optional[int] = None, + ) -> _get_version_info_ret: + ... def set_restart( - self, *, - delay: int = 0, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + delay: int = 0, + self_id: Optional[int] = None, + ) -> None: + ... def clean_cache( - self, *, - self_id: Optional[int] = None, - ) -> None: ... + self, + *, + self_id: Optional[int] = None, + ) -> None: + ... diff --git a/aiocqhttp/api_impl.py b/aiocqhttp/api_impl.py index 8faaada..b301492 100644 --- a/aiocqhttp/api_impl.py +++ b/aiocqhttp/api_impl.py @@ -4,6 +4,8 @@ import asyncio import sys +import uuid +import logging from typing import Callable, Dict, Any, Optional, Set, Union, Awaitable from .api import Api, AsyncApi, SyncApi @@ -13,6 +15,11 @@ except ImportError: import json +try: + import websockets +except ImportError: + websockets = None + import httpx from quart import websocket as event_ws from quart.wrappers.websocket import Websocket @@ -119,8 +126,7 @@ class WebSocketReverseApi(AsyncApi): """ def __init__(self, connected_api_clients: Dict[str, Websocket], - connected_event_clients: Set[Websocket], - timeout_sec: float): + connected_event_clients: Set[Websocket], timeout_sec: float): super().__init__() self._api_clients = connected_api_clients self._event_clients = connected_event_clients @@ -158,28 +164,39 @@ class UnifiedApi(AsyncApi): """ 统一 API 实现类。 - 同时维护 `HttpApi` 和 `WebSocketReverseApi` 对象,根据可用情况,选择两者中的某个使用。 + 同时维护 `HttpApi`、`WebSocketReverseApi` 和 `WebSocketForwardApi` 对象,根据可用情况选择使用。 """ def __init__(self, http_api: Optional[AsyncApi] = None, - wsr_api: Optional[AsyncApi] = None): + wsr_api: Optional[AsyncApi] = None, + wsf_api: Optional[AsyncApi] = None): super().__init__() self._http_api = http_api self._wsr_api = wsr_api + self._wsf_api = wsf_api async def call_action(self, action: str, **params) -> Any: result = None succeeded = False - if self._wsr_api: - # WebSocket is preferred + # Try forward WebSocket first (highest priority) + if self._wsf_api: + try: + result = await self._wsf_api.call_action(action, **params) + succeeded = True + except ApiNotAvailable: + pass + + # Try reverse WebSocket second + if not succeeded and self._wsr_api: try: result = await self._wsr_api.call_action(action, **params) succeeded = True except ApiNotAvailable: pass + # Try HTTP last if not succeeded and self._http_api: try: result = await self._http_api.call_action(action, **params) @@ -192,12 +209,187 @@ async def call_action(self, action: str, **params) -> Any: return result +def _is_websocket_url(url: str) -> bool: + """判断 ``url`` 是否为 WebSocket URL。""" + return url.startswith(('ws://', 'wss://')) + + +class WebSocketForwardApi(AsyncApi): + """ + 正向 WebSocket API 实现类。 + + 实现作为 WebSocket 客户端主动连接到 OneBot 服务器。 + """ + + def __init__(self, ws_url: str, access_token: Optional[str], + timeout_sec: float, event_handler: Optional[Callable]): + super().__init__() + if not websockets: + raise ImportError( + "websockets package is required for forward WebSocket support") + + self._ws_url = ws_url + self._access_token = access_token + self._timeout_sec = timeout_sec + self._event_handler = event_handler + self._connection = None + self._response_futures: Dict[str, asyncio.Future] = {} + self._running = False + self._connect_lock = asyncio.Lock() + self._logger = logging.getLogger(__name__) + self._auto_connect_task = None + self._schedule_auto_connect() + + def _schedule_auto_connect(self) -> None: + try: + loop = asyncio.get_running_loop() + except RuntimeError: + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = None + + if not loop: + self._logger.warning( + 'No event loop available for forward WebSocket auto-connect') + return + + try: + self._auto_connect_task = loop.create_task(self._auto_connect()) + except Exception as e: + self._logger.error( + f'Failed to schedule forward WebSocket auto-connect: {e}') + + async def _auto_connect(self) -> None: + try: + await self._ensure_connected() + except Exception as e: + self._logger.error(f'Forward WebSocket auto-connect failed: {e}') + + async def call_action(self, action: str, **params) -> Any: + await self._ensure_connected() + if not self._connection: + raise ApiNotAvailable + + echo = str(uuid.uuid4()) + future = asyncio.get_event_loop().create_future() + self._response_futures[echo] = future + + request = {'action': action, 'params': params, 'echo': echo} + + try: + await self._connection.send(json.dumps(request, ensure_ascii=False)) + response = await asyncio.wait_for(future, timeout=self._timeout_sec) + return _handle_api_result(response) + except asyncio.TimeoutError: + raise NetworkError('WebSocket API call timeout') + finally: + self._response_futures.pop(echo, None) + + async def _ensure_connected(self): + if self._is_connection_closed(): + async with self._connect_lock: + if self._is_connection_closed(): + await self._connect() + + def _is_connection_closed(self) -> bool: + if not self._connection: + return True + if hasattr(self._connection, "closed"): + return bool(self._connection.closed) + if hasattr(self._connection, "close_code"): + return self._connection.close_code is not None + state = getattr(self._connection, "state", None) + if state is not None: + return str(state).lower() in {"closing", "closed"} + return False + + async def _connect(self): + headers = {} + if self._access_token: + headers['Authorization'] = f'Bearer {self._access_token}' + + try: + try: + self._connection = await websockets.connect( + self._ws_url, + additional_headers=headers, + open_timeout=self._timeout_sec) + except Exception as e: + if "additional_headers" not in str( + e) and "open_timeout" not in str(e): + raise + try: + self._connection = await websockets.connect( + self._ws_url, + headers=headers, + open_timeout=self._timeout_sec) + except Exception as e2: + if "headers" not in str(e2) and "open_timeout" not in str( + e2): + raise + self._connection = await websockets.connect( + self._ws_url, + extra_headers=headers, + timeout=self._timeout_sec) + self._running = True + asyncio.create_task(self._message_loop()) + self._logger.info(f'Forward WebSocket connected to {self._ws_url}') + except Exception as e: + self._logger.error(f'Failed to connect to {self._ws_url}: {e}') + raise NetworkError(f'WebSocket connection failed: {e}') + + async def _message_loop(self): + try: + while self._running and self._connection: + try: + message = await self._connection.recv() + try: + data = json.loads(message) + await self._handle_message(data) + except json.JSONDecodeError: + self._logger.warning( + f'Received invalid JSON: {message}') + except websockets.exceptions.ConnectionClosed: + self._logger.info('Forward WebSocket connection closed') + break + except Exception as e: + self._logger.error(f'Error in message loop: {e}') + finally: + self._running = False + self._connection = None + + async def _handle_message(self, data: Dict[str, Any]): + # API responses have 'echo' field + if 'echo' in data and data['echo'] in self._response_futures: + future = self._response_futures[data['echo']] + if not future.done(): + future.set_result(data) + # OneBot events have 'post_type' field + elif 'post_type' in data and self._event_handler: + + async def _run_handler(): + try: + await self._event_handler(data) + except Exception as e: + self._logger.error(f'Error handling event: {e}') + + asyncio.create_task(_run_handler()) + + async def close(self): + self._running = False + if self._connection: + await self._connection.close() + self._connection = None + + class SyncWrapperApi(SyncApi): """ 封装 `AsyncApi` 对象,使其可同步地调用。 """ - def __init__(self, async_api: AsyncApi, + def __init__(self, + async_api: AsyncApi, loop: Optional[asyncio.AbstractEventLoop] = None): """ `async_api` 参数为 `AsyncApi` 对象,`loop` 参数为用来执行 API diff --git a/aiocqhttp/message.py b/aiocqhttp/message.py index 3faab01..6782810 100644 --- a/aiocqhttp/message.py +++ b/aiocqhttp/message.py @@ -8,7 +8,6 @@ from .typing import Message_T - __pdoc__ = {} @@ -173,6 +172,7 @@ def __radd__(self, other: Any) -> 'Message': __pdoc__['MessageSegment.__radd__'] = True if sys.version_info >= (3, 9, 0): + def __or__(self, other): raise NotImplementedError @@ -205,12 +205,12 @@ def image(file: str, # NOTE: destruct parameter is not part of the onebot v11 std. return MessageSegment(type_='image', data=_remove_optional({ - 'file': file, - 'type': _optionally_strfy(type), - 'cache': _optionally_strfy(cache), - 'proxy': _optionally_strfy(proxy), - 'timeout': _optionally_strfy(timeout), - 'destruct': _optionally_strfy(destruct), + 'file': file, + 'type': _optionally_strfy(type), + 'cache': _optionally_strfy(cache), + 'proxy': _optionally_strfy(proxy), + 'timeout': _optionally_strfy(timeout), + 'destruct': _optionally_strfy(destruct), })) @staticmethod @@ -268,8 +268,8 @@ def poke(type_: str, id_: int) -> 'MessageSegment': """戳一戳。""" return MessageSegment(type_='poke', data={ - 'type': type_, - 'id': str(id_), + 'type': type_, + 'id': str(id_), }) @staticmethod @@ -372,17 +372,17 @@ def node(id_: int) -> 'MessageSegment': return MessageSegment(type_='node', data={'id': str(id_)}) @staticmethod - def node_custom(user_id: int, - nickname: str, + def node_custom(user_id: int, nickname: str, content: Message_T) -> 'MessageSegment': """合并转发自定义节点。""" if not isinstance(content, (str, MessageSegment, Message)): content = Message(content) - return MessageSegment(type_='node', data={ - 'user_id': str(user_id), - 'nickname': nickname, - 'content': str(content), - }) + return MessageSegment(type_='node', + data={ + 'user_id': str(user_id), + 'nickname': nickname, + 'content': str(content), + }) @staticmethod def xml(data: str) -> 'MessageSegment': diff --git a/docs/getting-started.md b/docs/getting-started.md index c72aac0..eaa7b20 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -20,7 +20,13 @@ pip install aiocqhttp pip install aiocqhttp[all] ``` -这将会额外安装 `ujson`。 +这将会额外安装 `ujson` 和正向 WebSocket 所需的 `websockets`。 + +如果只需要正向 WebSocket 支持,可使用: + +```bash +pip install aiocqhttp[forward-ws] +``` ## 最小实例 @@ -76,6 +82,24 @@ Running on http://127.0.0.1:8080 (CTRL + C to quit) 最后重启 CQHTTP。 +### 使用正向 WebSocket + +修改 `bot.py` 中创建 `bot` 对象部分的代码为: + +```python +bot = CQHttp(api_root='ws://127.0.0.1:6700/') +``` + +这里 `127.0.0.1:6700` 应根据情况改为 OneBot 监听的 WebSocket 地址和端口。 + +如果 OneBot 配置中启用了鉴权,还需要传入 `access_token`: + +```python +bot = CQHttp(api_root='ws://127.0.0.1:6700/', access_token='your_token') +``` + +然后在 OneBot 配置中启用正向 WebSocket 并确保监听地址可访问,最后重启 OneBot。 + ### 使用 HTTP 修改 `bot.py` 中创建 `bot` 对象部分的代码为: diff --git a/scripts/gen_cqhttp_api_stub.py b/scripts/gen_cqhttp_api_stub.py index 84da135..3a96b48 100644 --- a/scripts/gen_cqhttp_api_stub.py +++ b/scripts/gen_cqhttp_api_stub.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from typing import List, Optional, Tuple - TypeStr = str type_mappings = { @@ -46,7 +45,8 @@ class ApiReturn: is_array: bool @property - def _base(self): return f'_{self.action}_ret' + def _base(self): + return f'_{self.action}_ret' @property def var_name(self): @@ -78,8 +78,8 @@ def _get_param_specs(self): if self.params is not None: params = 'self, *,\n' params += '\n'.join(f'{p.render()},' for p in self.params) - arg_docs = '\n'.join(f'{p.name}: {p.description}' - for p in self.params) + arg_docs = '\n'.join( + f'{p.name}: {p.description}' for p in self.params) if self.ret is not None: ret = self.ret.var_name return params, arg_docs, ret @@ -119,8 +119,8 @@ def create_params(param_block: str): rows = re.findall(r'^\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|', param_block, re.MULTILINE) else: # ^| 字段名 | 数据类型 | 说明 | - rows = re.findall(r'^\|([^|]+)\|([^|]+)\|([^|]+)\|', - param_block, re.MULTILINE) + rows = re.findall(r'^\|([^|]+)\|([^|]+)\|([^|]+)\|', param_block, + re.MULTILINE) for row in islice(rows, 2, None): name = row[0].split()[0].strip(' `') # `xxx` 或 `yyy` type_ = type_mappings[row[1].strip()] @@ -155,11 +155,12 @@ def create_ret(action: str, ret_block: str): first = True ret = None # there might be multiple tables - for table_block in re.findall(r'\|\s*字段名.+?(?=(?=\n\n)|(?=$))', - ret_block, re.DOTALL): + for table_block in re.findall(r'\|\s*字段名.+?(?=(?=\n\n)|(?=$))', ret_block, + re.DOTALL): fields = [] - for row in islice(re.findall(r'^\|([^|]+)\|([^|]+)\|([^|]+)\|', - table_block, re.MULTILINE), 2, None): + for row in islice( + re.findall(r'^\|([^|]+)\|([^|]+)\|([^|]+)\|', table_block, + re.MULTILINE), 2, None): name = row[0].strip(' `') if name == '……': name = '#__there_might_be_more_fields_below' @@ -205,13 +206,13 @@ def create_apis(fn: str) -> List[Api]: template_path = path.join(path.dirname(__file__), 'api.pyi.template') with open(template_path) as template_in, open(sys.argv[2], 'w') as of: apis = create_apis(sys.argv[1]) - api_returns_38 = '\n\n'.join(api.ret.render_definition_38() - for api in apis if api.ret is not None) - api_returns_37 = '\n'.join(api.ret.render_definition_37() - for api in apis if api.ret is not None) + api_returns_38 = '\n\n'.join( + api.ret.render_definition_38() for api in apis if api.ret is not None) + api_returns_37 = '\n'.join( + api.ret.render_definition_37() for api in apis if api.ret is not None) api_methods = '\n\n'.join(api.render_definition() for api in apis) - api_methods_async = '\n\n'.join(api.render_definition_async() - for api in apis) + api_methods_async = '\n\n'.join( + api.render_definition_async() for api in apis) api_methods_sync = '\n\n'.join(api.render_definition_sync() for api in apis) of.write(template_in.read().format( api_returns_38=indent(api_returns_38, ' ' * 4), diff --git a/setup.py b/setup.py index 7b7b4f6..82b77b4 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,8 @@ }, install_requires=['Quart>=0.17,<1.0', 'httpx>=0.11,<1.0'], extras_require={ - 'all': ['ujson'], + 'all': ['ujson', 'websockets>=8.0'], + 'forward-ws': ['websockets>=8.0'], }, python_requires='>=3.7', platforms='any',