diff --git a/README.md b/README.md index dd6fe61..53c9302 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev) [![PyPI Version](https://img.shields.io/pypi/v/nonebot-plugin-all4one.svg?style=flat-square)](https://pypi.python.org/pypi/nonebot-plugin-all4one) [![OneBot 4A](https://img.shields.io/badge/OneBot-4A-black?style=flat-square)](https://onebot4all.vercel.app/) -[![NoneBot Version](https://img.shields.io/badge/nonebot-2.3.0+-red.svg?style=flat-square)](https://v2.nonebot.dev/) +[![NoneBot Version](https://img.shields.io/badge/nonebot-2.3.0+-red.svg?style=flat-square)](https://v2.nonebot.dev/) [![NoneBot Registry](https://img.shields.io/endpoint?url=https%3A%2F%2Fnbbdg.lgc2333.top%2Fplugin%2Fnonebot-plugin-all4one)](https://registry.nonebot.dev/plugin/nonebot-plugin-all4one:nonebot_plugin_all4one) [![Supported Adapters](https://img.shields.io/endpoint?url=https%3A%2F%2Fnbbdg.lgc2333.top%2Fplugin-adapters%2Fnonebot-plugin-all4one)](https://registry.nonebot.dev/plugin/nonebot-plugin-all4one:nonebot_plugin_all4one) diff --git a/nonebot_plugin_all4one/__version__.py b/nonebot_plugin_all4one/__version__.py new file mode 100644 index 0000000..bf2e442 --- /dev/null +++ b/nonebot_plugin_all4one/__version__.py @@ -0,0 +1,3 @@ +import importlib.metadata as importlib_metadata + +__version__ = importlib_metadata.version(__package__ or "nonebot-plugin-all4one") diff --git a/nonebot_plugin_all4one/logger.py b/nonebot_plugin_all4one/logger.py new file mode 100644 index 0000000..133e5d4 --- /dev/null +++ b/nonebot_plugin_all4one/logger.py @@ -0,0 +1,3 @@ +from nonebot.utils import logger_wrapper + +log = logger_wrapper("041") diff --git a/nonebot_plugin_all4one/middlewares/__init__.py b/nonebot_plugin_all4one/middlewares/__init__.py index 12e6402..afcd614 100644 --- a/nonebot_plugin_all4one/middlewares/__init__.py +++ b/nonebot_plugin_all4one/middlewares/__init__.py @@ -2,8 +2,7 @@ from pathlib import Path from typing import Optional, cast -from nonebot.log import logger - +from ..logger import log from .base import Middleware MIDDLEWARE_MAP = {} @@ -23,4 +22,4 @@ ): MIDDLEWARE_MAP[middleware.get_name()] = middleware except Exception: - logger.warning(f"Failed to import {module_name}") + log("WARNING", f"Failed to import {module_name}") diff --git a/nonebot_plugin_all4one/onebotimpl/__init__.py b/nonebot_plugin_all4one/onebotimpl/__init__.py index c6766aa..9265b38 100644 --- a/nonebot_plugin_all4one/onebotimpl/__init__.py +++ b/nonebot_plugin_all4one/onebotimpl/__init__.py @@ -4,11 +4,10 @@ from functools import partial from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from typing import Any, Union, Literal, Optional, cast +from typing import Any, Union, Literal, ClassVar, Optional, cast from asyncio import Task, Queue, sleep, gather, wait_for, create_task import msgpack -from nonebot.log import logger from nonebot.adapters import Bot from nonebot.utils import escape_tag from nonebot.exception import WebSocketClosed @@ -38,7 +37,9 @@ WebSocketServerSetup, ) +from ..logger import log from .utils import encode_data +from ..__version__ import __version__ from ..middlewares import MIDDLEWARE_MAP, Middleware from .config import ( Config, @@ -50,6 +51,10 @@ class OneBotImplementation: + USER_AGENT: ClassVar[str] = f"OneBot4All NoneBot-Plugin-All4One/{__version__}" + ONEBOT_VERSION: ClassVar[str] = "A" + IMPL_NAME: ClassVar[str] = "nonebot-plugin-all4one" + def __init__(self, driver: Driver): self.driver = driver self.config = Config(**self.driver.config.model_dump()) @@ -89,14 +94,10 @@ def register_middleware(self, middleware: type[Middleware]): """注册一个中间件""" name = middleware.get_name() if name in self._middlewares: - logger.opt(colors=True).warning( - f'Middleware "{escape_tag(name)}" already exists' - ) + log("WARNING", f'Middleware "{escape_tag(name)}" already exists') return self._middlewares[name] = middleware - logger.opt(colors=True).info( - f'Succeeded to load middleware "{escape_tag(name)}"' - ) + log("INFO", f'Succeeded to load middleware "{escape_tag(name)}"') async def _call_api(self, data: dict[str, Any]) -> Any: try: @@ -190,9 +191,9 @@ async def get_version( kwargs: 扩展字段 """ return { - "impl": "nonebot-plugin-all4one", - "version": "0.1.0", - "onebot_version": "12", + "impl": self.IMPL_NAME, + "version": __version__, + "onebot_version": self.ONEBOT_VERSION, } def _check_access_token( @@ -215,20 +216,20 @@ async def _ws_send( websocket: WebSocket, conn: Union[WebsocketConfig, WebsocketReverseConfig], ) -> None: - queue = Queue() + queue = Queue[Event]() self.queues.append(queue) try: while True: event = await queue.get() - await websocket.send(encode_data(event.dict(), conn.use_msgpack)) + await websocket.send(encode_data(event.model_dump(), conn.use_msgpack)) except WebSocketClosed: - logger.opt(colors=True).warning( - "WebSocket Closed" - ) - except Exception: - logger.opt(colors=True).exception( - "Error while process data from websocket" - ". Trying to reconnect..." + log("WARNING", "WebSocket Closed") + except Exception as e: + log( + "ERROR", + "Error while process data from websocket. " + "Trying to reconnect...", + e, ) finally: self.queues.remove(queue) @@ -247,7 +248,7 @@ async def _ws_recv(self, websocket: WebSocket) -> None: if "echo" in data: echo = data["echo"] resp = await self._call_api(data) - # 格式错误(包括实现不支持 MessagePack 的情况)、必要字段缺失或字段类型错误 + # 格式错误(包括实现不支持 MessagePack 的情况)、必要字段缺失或类型错误 except (json.JSONDecodeError, msgpack.UnpackException): resp = { "status": "failed", @@ -267,11 +268,13 @@ async def _ws_recv(self, websocket: WebSocket) -> None: resp["echo"] = echo await websocket.send(encode_data(resp, isinstance(raw_data, bytes))) except WebSocketClosed: - logger.opt(colors=True).warning("WebSocket closed by peer") + log("WARNING", "WebSocket closed by peer") # 与 WebSocket 服务器的连接发生了意料之外的错误 - except Exception: - logger.opt(colors=True).exception( - "Error while process data from websocket" + except Exception as e: + log( + "ERROR", + "Error while process data from websocket", + e, ) async def _handle_http( @@ -283,7 +286,7 @@ async def _handle_http( if response := self._check_access_token(request, conn.access_token): return response - # 如果收到不支持的 Content-Type 请求头,必须返回 HTTP 状态码 415 Unsupported Media Type + # 如果收到不支持的 Content-Type 请求头,必须返回 HTTP 状态码 415 content_type = request.headers.get("Content-Type") if content_type not in ("application/json", "application/msgpack"): return Response(415, content="Invalid Content-Type") @@ -341,6 +344,19 @@ async def _handle_ws(self, conn: WebsocketConfig, websocket: WebSocket) -> None: conn.use_msgpack, ) ) + await websocket.send( + encode_data( + StatusUpdateMetaEvent( + id=uuid.uuid4().hex, + time=datetime.now(), + type="meta", + detail_type="status_update", + sub_type="", + status=await self.get_status(), + ).model_dump(), + conn.use_msgpack, + ) + ) t1 = create_task(self._ws_send(websocket, conn)) t2 = create_task(self._ws_recv(websocket)) await t2 @@ -351,14 +367,24 @@ async def _http_webhook(self, conn: HTTPWebhookConfig): "Content-Type": ( "application/msgpack" if conn.use_msgpack else "application/json" ), - "User-Agent": "OneBot/12 NoneBot Plugin All4One/0.1.0", - "X-OneBot-Version": "12", - "X-Impl": "nonebot-plugin-all4one", + "User-Agent": self.USER_AGENT, + "X-OneBot-Version": self.ONEBOT_VERSION, + "X-Impl": self.IMPL_NAME, } if conn.access_token: headers["Authorization"] = f"Bearer {conn.access_token}" - queue = Queue() + queue = Queue[Event]() self.queues.append(queue) + await queue.put( + StatusUpdateMetaEvent( + id=uuid.uuid4().hex, + time=datetime.now(), + type="meta", + detail_type="status_update", + sub_type="", + status=await self.get_status(), + ) + ) while True: try: event = await queue.get() @@ -366,7 +392,7 @@ async def _http_webhook(self, conn: HTTPWebhookConfig): "POST", str(conn.url), headers=headers, - content=encode_data(event.dict(), conn.use_msgpack), + content=encode_data(event.model_dump(), conn.use_msgpack), ) resp = await self.request(request) if resp.status_code == 200: @@ -380,32 +406,33 @@ async def _http_webhook(self, conn: HTTPWebhookConfig): elif content_type == "application/json": data = json.loads(resp.content) else: - logger.error("Invalid Content-Type") + log("ERROR", "Invalid Content-Type") continue for action in data: await self._call_api(action) # 动作请求执行出错 - except Exception: - logger.exception("HTTP Webhook Response action failed") + except Exception as e: + log("ERROR", "HTTP Webhook Response action failed", e) # 事件推送成功,并不做更多处理 elif resp.status_code == 204: pass # 事件推送失败 else: - logger.error(f"HTTP Webhook event push failed: {resp}") + log("ERROR", f"HTTP Webhook event push failed: {resp}") except (NotImplementedError, TypeError): - logger.error( - f"Current driver {self.driver.type} does not support http client" + log( + "ERROR", + f"Current driver {self.driver.type} does not support http client", ) self.queues.remove(queue) break - except Exception: - logger.exception("HTTP Webhook event push failed") + except Exception as e: + log("ERROR", "HTTP Webhook event push failed", e) async def _websocket_rev(self, conn: WebsocketReverseConfig) -> None: headers = { - "User-Agent": "OneBot/12 NoneBot Plugin All4One/0.1.0", - "Sec-WebSocket-Protocol": "12.nonebot-plugin-all4one", + "User-Agent": self.USER_AGENT, + "Sec-WebSocket-Protocol": f"{self.ONEBOT_VERSION}.{self.IMPL_NAME}", } if conn.access_token: headers["Authorization"] = f"Bearer {conn.access_token}" @@ -432,28 +459,44 @@ async def _websocket_rev(self, conn: WebsocketReverseConfig) -> None: conn.use_msgpack, ) ) + await ws.send( + encode_data( + StatusUpdateMetaEvent( + id=uuid.uuid4().hex, + time=datetime.now(), + type="meta", + detail_type="status_update", + sub_type="", + status=await self.get_status(), + ).model_dump(), + conn.use_msgpack, + ) + ) t1 = create_task(self._ws_send(ws, conn)) t2 = create_task(self._ws_recv(ws)) await t2 t1.cancel() except WebSocketClosed: - logger.opt(colors=True).warning( - "WebSocket Closed" - ) - except Exception: - logger.opt(colors=True).exception( - "Error while process data from websocket" - f"{escape_tag(str(conn.url))}. Trying to reconnect...", + log("WARNING", "WebSocket Closed") + except Exception as e: + log( + "ERROR", + "Error while process data from websocket" + f"{escape_tag(str(conn.url))}. Trying to reconnect...", + e, ) except (NotImplementedError, TypeError): - logger.error( - f"Current driver {self.driver.type} does not support websocket server" + log( + "ERROR", + f"Current driver {self.driver.type} " + "does not support websocket server", ) break except Exception: - logger.opt(colors=True).warning( - "Error while setup websocket to " - f"{escape_tag(str(conn.url))}. Trying to reconnect...", + log( + "WARNING", + "Error while setup websocket to " + f"{escape_tag(str(conn.url))}. Trying to reconnect...", ) await sleep(conn.reconnect_interval) @@ -502,7 +545,7 @@ def _register_middlewares(self, middlewares: Optional[set[str]] = None): if middleware in MIDDLEWARE_MAP: self.register_middleware(MIDDLEWARE_MAP[middleware]) else: - logger.error(f"Can not find middleware for Adapter {middleware}") + log("ERROR", f"Can not find middleware for Adapter {middleware}") def setup(self): @self.driver.on_startup @@ -512,7 +555,7 @@ async def _(): if isinstance(conn, HTTPConfig): queue = None if conn.event_enabled: - queue = Queue(conn.event_buffer_size) + queue = Queue[Event](conn.event_buffer_size) self.setup_http_server( HTTPServerSetup( URL("/all4one/"),