diff --git a/nonebot/adapters/mirai/__init__.py b/nonebot/adapters/mirai/__init__.py index 1107af38..b209657e 100644 --- a/nonebot/adapters/mirai/__init__.py +++ b/nonebot/adapters/mirai/__init__.py @@ -1,3 +1,13 @@ +""" +Mirai-API-HTTP 协议适配 +============================ + +协议详情请看: `mirai-api-http 文档`_ + +.. mirai-api-http 文档: + https://github.com/project-mirai/mirai-api-http/tree/master/docs +""" + from .bot import MiraiBot from .bot_ws import MiraiWebsocketBot from .event import * diff --git a/nonebot/adapters/mirai/bot.py b/nonebot/adapters/mirai/bot.py index ee718997..74c4f602 100644 --- a/nonebot/adapters/mirai/bot.py +++ b/nonebot/adapters/mirai/bot.py @@ -32,6 +32,7 @@ class ActionFailed(BaseActionFailed): class SessionManager: + """Bot会话管理器, 提供API主动调用接口""" sessions: Dict[int, Tuple[str, datetime, httpx.AsyncClient]] = {} session_expiry: timedelta = timedelta(minutes=15) @@ -40,19 +41,39 @@ class SessionManager: @staticmethod def _raise_code(data: Dict[str, Any]) -> Dict[str, Any]: - code = data.get('code', 0) - logger.opt(colors=True).debug('Mirai API returned data: ' - f'{escape_tag(str(data))}') - if code != 0: - raise ActionFailed(**data) + logger.opt(colors=True).debug( + f'Mirai API returned data: {escape_tag(str(data))}') + if isinstance(data, dict) and ('code' in data): + if data['code'] != 0: + raise ActionFailed(**data) return data async def post(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - params = {**(params or {}), 'sessionKey': self.session_key} - response = await self.client.post(path, json=params, timeout=3) + """ + :说明: + + 以POST方式主动提交API请求 + + :参数: + + * ``path: str``: 对应API路径 + * ``params: Optional[Dict[str, Any]]``: 请求参数 (无需sessionKey) + + :返回: + + - ``Dict[str, Any]``: API 返回值 + """ + response = await self.client.post( + path, + json={ + **(params or {}), + 'sessionKey': self.session_key, + }, + timeout=3, + ) response.raise_for_status() return self._raise_code(response.json()) @@ -61,12 +82,28 @@ class SessionManager: *, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - response = await self.client.get(path, - params={ - **(params or {}), 'sessionKey': - self.session_key - }, - timeout=3) + """ + :说明: + + 以GET方式主动提交API请求 + + :参数: + + * ``path: str``: 对应API路径 + * ``params: Optional[Dict[str, Any]]``: 请求参数 (无需sessionKey) + + :返回: + + - ``Dict[str, Any]``: API 返回值 + """ + response = await self.client.get( + path, + params={ + **(params or {}), + 'sessionKey': self.session_key, + }, + timeout=3, + ) response.raise_for_status() return self._raise_code(response.json()) @@ -108,11 +145,11 @@ class SessionManager: return cls(session_key, client) @classmethod - def get(cls, self_id: int): + def get(cls, self_id: int, check_expire: bool = True): if self_id not in cls.sessions: return None key, time, client = cls.sessions[self_id] - if datetime.now() - time > cls.session_expiry: + if check_expire and (datetime.now() - time > cls.session_expiry): return None return cls(key, client) @@ -129,7 +166,6 @@ class MiraiBot(BaseBot): *, websocket: Optional[WebSocket] = None): super().__init__(connection_type, self_id, websocket=websocket) - self.api = SessionManager.get(int(self_id)) @property @overrides(BaseBot) @@ -140,6 +176,13 @@ class MiraiBot(BaseBot): def alive(self) -> bool: return not self.websocket.closed + @property + def api(self) -> SessionManager: + """返回该Bot对象的会话管理实例以提供API主动调用""" + api = SessionManager.get(self_id=int(self.self_id)) + assert api is not None, 'SessionManager has not been initialized' + return api + @classmethod @overrides(BaseBot) async def check_permission(cls, driver: "Driver", connection_type: str, @@ -179,10 +222,12 @@ class MiraiBot(BaseBot): @overrides(BaseBot) async def call_api(self, api: str, **data) -> NoReturn: + """由于Mirai的HTTP API特殊性, 该API暂时无法实现""" raise NotImplementedError @overrides(BaseBot) def __getattr__(self, key: str) -> NoReturn: + """由于Mirai的HTTP API特殊性, 该API暂时无法实现""" raise NotImplementedError @overrides(BaseBot) diff --git a/nonebot/adapters/mirai/bot_ws.py b/nonebot/adapters/mirai/bot_ws.py index 560534b6..ccce63b3 100644 --- a/nonebot/adapters/mirai/bot_ws.py +++ b/nonebot/adapters/mirai/bot_ws.py @@ -107,6 +107,12 @@ class MiraiWebsocketBot(MiraiBot): def alive(self) -> bool: return not self.websocket.closed + @property + def api(self) -> SessionManager: + api = SessionManager.get(self_id=int(self.self_id), check_expire=False) + assert api is not None, 'SessionManager has not been initialized' + return api + @classmethod @overrides(MiraiBot) async def check_permission(cls, driver: "Driver", connection_type: str, @@ -118,12 +124,23 @@ class MiraiWebsocketBot(MiraiBot): @classmethod @overrides(MiraiBot) def register(cls, driver: "Driver", config: "Config", qq: int): - cls.mirai_config = MiraiConfig(**config.dict()) - cls.active = True - assert cls.mirai_config.auth_key is not None - assert cls.mirai_config.host is not None - assert cls.mirai_config.port is not None + """ + :说明: + + 注册该Adapter + + :参数: + + * ``driver: Driver``: 程序所使用的``Driver`` + * ``config: Config``: 程序配置对象 + * ``qq: int``: 要使用的Bot的QQ号 **注意: 在使用正向Websocket时必须指定该值!** + + :返回: + + - ``[type]``: [description] + """ super().register(driver, config) + cls.active = True async def _bot_connection(): session: SessionManager = await SessionManager.new( diff --git a/nonebot/adapters/mirai/config.py b/nonebot/adapters/mirai/config.py index 942cf9fa..a907dd17 100644 --- a/nonebot/adapters/mirai/config.py +++ b/nonebot/adapters/mirai/config.py @@ -5,6 +5,15 @@ from pydantic import BaseModel, Extra, Field class Config(BaseModel): + """ + Mirai 配置类 + + :必填: + + - ``mirai_auth_key``: mirai-api-http的auth_key + - ``mirai_host``: mirai-api-http的地址 + - ``mirai_port``: mirai-api-http的端口 + """ auth_key: Optional[str] = Field(None, alias='mirai_auth_key') host: Optional[IPv4Address] = Field(None, alias='mirai_host') port: Optional[int] = Field(None, alias='mirai_port') diff --git a/nonebot/adapters/mirai/event/__init__.py b/nonebot/adapters/mirai/event/__init__.py index 8b96ecca..c0024e19 100644 --- a/nonebot/adapters/mirai/event/__init__.py +++ b/nonebot/adapters/mirai/event/__init__.py @@ -1,3 +1,10 @@ +""" +\:\:\:warning 警告 +事件中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则取代Mirai原有的驼峰命名 + +部分字段可能与文档在符号上不一致 +\:\:\: +""" from .base import Event, GroupChatInfo, GroupInfo, UserPermission, PrivateChatInfo from .message import * from .notice import * diff --git a/nonebot/adapters/mirai/event/base.py b/nonebot/adapters/mirai/event/base.py index 5b1cf7fc..662b856d 100644 --- a/nonebot/adapters/mirai/event/base.py +++ b/nonebot/adapters/mirai/event/base.py @@ -45,9 +45,9 @@ class PrivateChatInfo(BaseModel): class Event(BaseEvent): """ - mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 `mirai-api-http 文档`_ + mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 `mirai-api-http 事件类型`_ - .. _mirai-api-http 文档: + .. _mirai-api-http 事件类型: https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md """ self_id: int diff --git a/nonebot/adapters/mirai/event/request.py b/nonebot/adapters/mirai/event/request.py index cea13aae..18d466ee 100644 --- a/nonebot/adapters/mirai/event/request.py +++ b/nonebot/adapters/mirai/event/request.py @@ -55,10 +55,6 @@ class NewFriendRequestEvent(RequestEvent): - ``1``: 拒绝添加好友 - ``2``: 拒绝添加好友并添加黑名单,不再接收该用户的好友申请 * ``message: str``: 回复的信息 - - :返回: - - - ``[type]``: [description] """ assert operate > 0 return await bot.api.post('/resp/newFriendRequestEvent', diff --git a/nonebot/adapters/mirai/message.py b/nonebot/adapters/mirai/message.py index a577a807..265b3b3b 100644 --- a/nonebot/adapters/mirai/message.py +++ b/nonebot/adapters/mirai/message.py @@ -9,6 +9,7 @@ from nonebot.typing import overrides class MessageType(str, Enum): + """消息类型枚举类""" SOURCE = 'Source' QUOTE = 'Quote' AT = 'At' @@ -25,6 +26,13 @@ class MessageType(str, Enum): class MessageSegment(BaseMessageSegment): + """ + CQHTTP 协议 MessageSegment 适配。具体方法参考 `mirai-api-http 消息类型`_ + + .. _mirai-api-http 消息类型: + https://github.com/project-mirai/mirai-api-http/blob/master/docs/MessageType.md + """ + type: MessageType data: Dict[str, Any] @@ -59,6 +67,7 @@ class MessageSegment(BaseMessageSegment): return self.type == MessageType.PLAIN def as_dict(self) -> Dict[str, Any]: + """导出可以被正常json序列化的结构体""" return {'type': self.type.value, **self.data} @classmethod @@ -68,6 +77,19 @@ class MessageSegment(BaseMessageSegment): @classmethod def quote(cls, id: int, group_id: int, sender_id: int, target_id: int, origin: "MessageChain"): + """ + :说明: + + 生成回复引用消息段 + + :参数: + + * ``id: int``: 被引用回复的原消息的message_id + * ``group_id: int``: 被引用回复的原消息所接收的群号,当为好友消息时为0 + * ``sender_id: int``: 被引用回复的原消息的发送者的QQ号 + * ``target_id: int``: 被引用回复的原消息的接收者者的QQ号(或群号) + * ``origin: MessageChain``: 被引用回复的原消息的消息链对象 + """ return cls(type=MessageType.QUOTE, id=id, groupId=group_id, @@ -77,18 +99,51 @@ class MessageSegment(BaseMessageSegment): @classmethod def at(cls, target: int): + """ + :说明: + + @某个人 + + :参数: + + * ``target: int``: 群员QQ号 + """ return cls(type=MessageType.AT, target=target) @classmethod def at_all(cls): + """ + :说明: + + @全体成员 + """ return cls(type=MessageType.AT_ALL) @classmethod def face(cls, face_id: Optional[int] = None, name: Optional[str] = None): + """ + :说明: + + 发送QQ表情 + + :参数: + + * ``face_id: Optional[int]``: QQ表情编号,可选,优先高于name + * ``name: Optional[str]``: QQ表情拼音,可选 + """ return cls(type=MessageType.FACE, faceId=face_id, name=name) @classmethod def plain(cls, text: str): + """ + :说明: + + 纯文本消息 + + :参数: + + * ``text: str``: 文字消息 + """ return cls(type=MessageType.PLAIN, text=text) @classmethod @@ -96,6 +151,21 @@ class MessageSegment(BaseMessageSegment): image_id: Optional[str] = None, url: Optional[str] = None, path: Optional[str] = None): + """ + :说明: + + 图片消息 + + :参数: + + * ``image_id: Optional[str]``: 图片的image_id,群图片与好友图片格式不同。不为空时将忽略url属性 + * ``url: Optional[str]``: 图片的URL,发送时可作网络图片的链接 + * ``path: Optional[str]``: 图片的路径,发送本地图片 + + :返回: + + - ``[type]``: [description] + """ return cls(type=MessageType.IMAGE, imageId=image_id, url=url, path=path) @classmethod @@ -103,6 +173,15 @@ class MessageSegment(BaseMessageSegment): image_id: Optional[str] = None, url: Optional[str] = None, path: Optional[str] = None): + """ + :说明: + + 闪照消息 + + :参数: + + 同 ``image`` + """ return cls(type=MessageType.FLASH_IMAGE, imageId=image_id, url=url, @@ -113,6 +192,17 @@ class MessageSegment(BaseMessageSegment): voice_id: Optional[str] = None, url: Optional[str] = None, path: Optional[str] = None): + """ + :说明: + + 语音消息 + + :参数: + + * ``voice_id: Optional[str]``: 语音的voice_id,不为空时将忽略url属性 + * ``url: Optional[str]``: 语音的URL,发送时可作网络语音的链接 + * ``path: Optional[str]``: 语音的路径,发送本地语音 + """ return cls(type=MessageType.FLASH_IMAGE, imageId=voice_id, url=url, @@ -120,22 +210,69 @@ class MessageSegment(BaseMessageSegment): @classmethod def xml(cls, xml: str): + """ + :说明: + + XML消息 + + :参数: + + * ``xml: str``: XML文本 + """ return cls(type=MessageType.XML, xml=xml) @classmethod def json(cls, json: str): + """ + :说明: + + Json消息 + + :参数: + + * ``json: str``: Json文本 + """ return cls(type=MessageType.JSON, json=json) @classmethod def app(cls, content: str): + """ + :说明: + + 应用程序消息 + + :参数: + + * ``content: str``: 内容 + """ return cls(type=MessageType.APP, content=content) @classmethod def poke(cls, name: str): + """ + :说明: + + 戳一戳消息 + + :参数: + + * ``name: str``: 戳一戳的类型 + - "Poke": 戳一戳 + - "ShowLove": 比心 + - "Like": 点赞 + - "Heartbroken": 心碎 + - "SixSixSix": 666 + - "FangDaZhao": 放大招 + """ return cls(type=MessageType.POKE, name=name) -class MessageChain(BaseMessage): #type:List[MessageSegment] +class MessageChain(BaseMessage): + """ + Mirai 协议 Messaqge 适配 + + 由于Mirai协议的Message实现较为特殊, 故使用MessageChain命名 + """ @overrides(BaseMessage) def __init__(self, message: Union[List[Dict[str, Any]], @@ -166,6 +303,7 @@ class MessageChain(BaseMessage): #type:List[MessageSegment] ] def export(self) -> List[Dict[str, Any]]: + """导出为可以被正常json序列化的数组""" return [ *map(lambda segment: segment.as_dict(), self.copy()) # type: ignore ]