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
]