diff --git a/README.md b/README.md
index 21f6276d..10633bdd 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ _✨ Python 异步机器人框架 ✨_
-
+
@@ -71,7 +71,7 @@ NoneBot2 的驱动框架 `Driver` 以及通信协议 `Adapter` 均可**自定义
目前 NoneBot2 内置的协议适配:
- [OneBot(CQHTTP) 协议](https://github.com/howmanybots/onebot/blob/master/README.md) (QQ 等)
-- [钉钉](https://ding-doc.dingtalk.com/doc#/serverapi2/krgddi) _开发中_
+- [钉钉](https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p) _开发中_
- [Telegram](https://core.telegram.org/bots/api) _计划中_
## 即刻开始
diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py
index 22c6f587..aca5ab8e 100644
--- a/nonebot/adapters/__init__.py
+++ b/nonebot/adapters/__init__.py
@@ -335,22 +335,21 @@ class Message(list, abc.ABC):
"""消息数组"""
def __init__(self,
- message: Union[str, dict, list, T_MessageSegment,
- T_Message] = None,
+ message: Union[T_MessageSegment, T_Message, Any] = None,
*args,
**kwargs):
"""
:参数:
- * ``message: Union[str, dict, list, MessageSegment, Message]``: 消息内容
+ * ``message: Union[MessageSegment, Message, Any]``: 消息内容
"""
super().__init__(*args, **kwargs)
- if isinstance(message, (str, dict, list)):
- self.extend(self._construct(message))
- elif isinstance(message, Message):
+ if isinstance(message, Message):
self.extend(message)
elif isinstance(message, MessageSegment):
self.append(message)
+ else:
+ self.extend(self._construct(message))
def __str__(self):
return ''.join((str(seg) for seg in self))
@@ -365,9 +364,7 @@ class Message(list, abc.ABC):
@staticmethod
@abc.abstractmethod
- def _construct(
- msg: Union[str, dict, list,
- BaseModel]) -> Iterable[T_MessageSegment]:
+ def _construct(msg: Union[Any]) -> Iterable[T_MessageSegment]:
raise NotImplementedError
def __add__(self: T_Message, other: Union[str, T_MessageSegment,
diff --git a/nonebot/adapters/ding/__init__.py b/nonebot/adapters/ding/__init__.py
index 4eb33e28..ea076c6f 100644
--- a/nonebot/adapters/ding/__init__.py
+++ b/nonebot/adapters/ding/__init__.py
@@ -5,7 +5,7 @@
协议详情请看: `钉钉文档`_
.. _钉钉文档:
- https://ding-doc.dingtalk.com/doc#/serverapi2/krgddi
+ https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p/
"""
diff --git a/nonebot/adapters/ding/bot.py b/nonebot/adapters/ding/bot.py
index c5ba4b61..18dc5b69 100644
--- a/nonebot/adapters/ding/bot.py
+++ b/nonebot/adapters/ding/bot.py
@@ -11,8 +11,8 @@ from nonebot.adapters import Bot as BaseBot
from nonebot.exception import RequestDenied
from .utils import log
-from .event import Event
-from .model import MessageModel
+from .event import Event, MessageEvent, PrivateMessageEvent, GroupMessageEvent
+from .model import ConversationType
from .message import Message, MessageSegment
from .exception import NetworkError, ApiNotAvailable, ActionFailed, SessionExpired
@@ -50,7 +50,8 @@ class Bot(BaseBot):
# 检查连接方式
if connection_type not in ["http"]:
- raise RequestDenied(405, "Unsupported connection type")
+ raise RequestDenied(
+ 405, "Unsupported connection type, available type: `http`")
# 检查 timestamp
if not timestamp:
@@ -73,22 +74,30 @@ class Bot(BaseBot):
return body["chatbotUserId"]
async def handle_message(self, body: dict):
- message = MessageModel.parse_obj(body)
- if not message:
+ if not body:
+ return
+
+ # 判断消息类型,生成不同的 Event
+ conversation_type = body["conversationType"]
+ if conversation_type == ConversationType.private:
+ event = PrivateMessageEvent.parse_obj(body)
+ else:
+ event = GroupMessageEvent.parse_obj(body)
+
+ if not event:
return
try:
- event = Event(message)
await handle_event(self, event)
except Exception as e:
logger.opt(colors=True, exception=e).error(
- f"Failed to handle event. Raw: {message}"
+ f"Failed to handle event. Raw: {event}"
)
return
async def call_api(self,
api: str,
- event: Optional[Event] = None,
+ event: Optional[MessageEvent] = None,
**data) -> Any:
"""
:说明:
@@ -124,10 +133,10 @@ class Bot(BaseBot):
if event:
# 确保 sessionWebhook 没有过期
if int(datetime.now().timestamp()) > int(
- event.raw_event.sessionWebhookExpiredTime / 1000):
+ event.sessionWebhookExpiredTime / 1000):
raise SessionExpired
- target = event.raw_event.sessionWebhook
+ target = event.sessionWebhook
else:
target = None
diff --git a/nonebot/adapters/ding/event.py b/nonebot/adapters/ding/event.py
index 87c16f25..507e9ccc 100644
--- a/nonebot/adapters/ding/event.py
+++ b/nonebot/adapters/ding/event.py
@@ -1,197 +1,84 @@
from typing import Union, Optional
+from typing_extensions import Literal
+
+from pydantic import BaseModel, validator, parse_obj_as
+from pydantic.fields import ModelField
from nonebot.adapters import Event as BaseEvent
+from nonebot.utils import escape_tag
from .message import Message
-from .model import MessageModel, ConversationType, TextMessage
+from .model import MessageModel, PrivateMessageModel, GroupMessageModel, ConversationType, TextMessage
class Event(BaseEvent):
"""
钉钉 协议 Event 适配。继承属性参考 `BaseEvent <./#class-baseevent>`_ 。
"""
+ message: Message = None
- def __init__(self, message: MessageModel):
- super().__init__(message)
+ def __init__(self, **data):
+ super().__init__(**data)
# 其实目前钉钉机器人只能接收到 text 类型的消息
- self._message = Message(getattr(message, message.msgtype or "text"))
+ message: Union[TextMessage] = getattr(self, self.msgtype, None)
+ self.message = parse_obj_as(Message, message)
- @property
- def raw_event(self) -> MessageModel:
- """原始上报消息"""
- return self._raw_event
-
- @property
- def id(self) -> Optional[str]:
- """
- - 类型: ``Optional[str]``
- - 说明: 消息 ID
- """
- return self.raw_event.msgId
-
- @property
- def name(self) -> str:
- """
- - 类型: ``str``
- - 说明: 事件名称,由 `type`.`detail_type` 组合而成
- """
- return self.type + "." + self.detail_type
-
- @property
- def self_id(self) -> str:
- """
- - 类型: ``str``
- - 说明: 机器人自身 ID
- """
- return str(self.raw_event.chatbotUserId)
-
- @property
- def time(self) -> int:
- """
- - 类型: ``int``
- - 说明: 消息的时间戳,单位 s
- """
- # 单位 ms -> s
- return int(self.raw_event.createAt / 1000)
-
- @property
- def type(self) -> str:
+ def get_type(self) -> Literal["message"]:
"""
- 类型: ``str``
- 说明: 事件类型
"""
return "message"
- @type.setter
- def type(self, value) -> None:
- pass
+ def get_event_name(self) -> str:
+ detail_type = self.conversationType.name
+ return self.get_type() + "." + detail_type
- @property
- def detail_type(self) -> str:
+ def get_event_description(self) -> str:
+ return (f'Message[{self.msgtype}] {self.msgId} from {self.senderId} "' +
+ "".join(
+ map(
+ lambda x: escape_tag(str(x))
+ if x.is_text() else f"{escape_tag(str(x))}",
+ self.message,
+ )) + '"')
+
+ def get_user_id(self) -> str:
+ return self.senderId
+
+ def get_session_id(self) -> str:
"""
- 类型: ``str``
- - 说明: 事件详细类型
+ - 说明: 消息 ID
"""
- return self.raw_event.conversationType.name
+ return self.msgId
- @detail_type.setter
- def detail_type(self, value) -> None:
- if value == "private":
- self.raw_event.conversationType = ConversationType.private
- if value == "group":
- self.raw_event.conversationType = ConversationType.group
-
- @property
- def sub_type(self) -> None:
+ def get_message(self) -> "Message":
"""
- - 类型: ``None``
- - 说明: 钉钉适配器无事件子类型
- """
- return None
-
- @sub_type.setter
- def sub_type(self, value) -> None:
- pass
-
- @property
- def user_id(self) -> Optional[str]:
- """
- - 类型: ``Optional[str]``
- - 说明: 发送者 ID
- """
- return self.raw_event.senderId
-
- @user_id.setter
- def user_id(self, value) -> None:
- self.raw_event.senderId = value
-
- @property
- def group_id(self) -> Optional[str]:
- """
- - 类型: ``Optional[str]``
- - 说明: 事件主体群 ID
- """
- return self.raw_event.conversationId
-
- @group_id.setter
- def group_id(self, value) -> None:
- self.raw_event.conversationId = value
-
- @property
- def to_me(self) -> Optional[bool]:
- """
- - 类型: ``Optional[bool]``
- - 说明: 消息是否与机器人相关
- """
- return self.detail_type == "private" or self.raw_event.isInAtList
-
- @property
- def message(self) -> Optional["Message"]:
- """
- - 类型: ``Optional[Message]``
+ - 类型: ``Message``
- 说明: 消息内容
"""
- return self._message
+ return self.message
- @message.setter
- def message(self, value) -> None:
- self._message = value
-
- @property
- def reply(self) -> None:
+ def get_plaintext(self) -> str:
"""
- - 类型: ``None``
- - 说明: 回复消息详情
- """
- raise ValueError("暂不支持 reply")
-
- @property
- def raw_message(self) -> Optional[Union[TextMessage]]:
- """
- - 类型: ``Optional[str]``
- - 说明: 原始消息
- """
- return getattr(self.raw_event, self.raw_event.msgtype)
-
- @raw_message.setter
- def raw_message(self, value) -> None:
- setattr(self.raw_event, self.raw_event.msgtype, value)
-
- @property
- def plain_text(self) -> Optional[str]:
- """
- - 类型: ``Optional[str]``
+ - 类型: ``str``
- 说明: 纯文本消息内容
"""
- return self.message and self.message.extract_plain_text().strip()
+ return self.message.extract_plain_text().strip() if self.message else ""
- @property
- def sender(self) -> Optional[dict]:
- """
- - 类型: ``Optional[dict]``
- - 说明: 消息发送者信息
- """
- result = {
- # 加密的发送者ID。
- "senderId": self.raw_event.senderId,
- # 发送者昵称。
- "senderNick": self.raw_event.senderNick,
- # 企业内部群有的发送者当前群的企业 corpId。
- "senderCorpId": self.raw_event.senderCorpId,
- # 企业内部群有的发送者在企业内的 userId。
- "senderStaffId": self.raw_event.senderStaffId,
- "role": "admin" if self.raw_event.isAdmin else "member"
- }
- return result
- @sender.setter
- def sender(self, value) -> None:
+class MessageEvent(MessageModel, Event):
+ pass
- def set_wrapper(name):
- if value.get(name):
- setattr(self.raw_event, name, value.get(name))
- set_wrapper("senderId")
- set_wrapper("senderNick")
- set_wrapper("senderCorpId")
- set_wrapper("senderStaffId")
+class PrivateMessageEvent(PrivateMessageModel, Event):
+
+ def is_tome(self) -> bool:
+ return True
+
+
+class GroupMessageEvent(GroupMessageModel, Event):
+
+ def is_tome(self) -> bool:
+ return self.isInAtList
diff --git a/nonebot/adapters/ding/message.py b/nonebot/adapters/ding/message.py
index 7fe53e88..cf6f56c0 100644
--- a/nonebot/adapters/ding/message.py
+++ b/nonebot/adapters/ding/message.py
@@ -37,6 +37,12 @@ class MessageSegment(BaseMessageSegment):
return MessageSegment.from_segment(self)
return Message(self) + other
+ def __radd__(self, other) -> "Message":
+ return Message(other) + self
+
+ def is_text(self) -> bool:
+ return self.type == "text"
+
def atMobile(self, mobileNumber):
self.data.setdefault("at", {})
self.data["at"].setdefault("atMobiles", [])
@@ -118,6 +124,10 @@ class Message(BaseMessage):
钉钉 协议 Message 适配。
"""
+ @classmethod
+ def _validate(cls, value):
+ return cls(value)
+
@staticmethod
def _construct(
msg: Union[str, dict, list,
diff --git a/nonebot/adapters/ding/model.py b/nonebot/adapters/ding/model.py
index 8f0cbe1c..49e4b0f5 100644
--- a/nonebot/adapters/ding/model.py
+++ b/nonebot/adapters/ding/model.py
@@ -26,23 +26,31 @@ class ConversationType(str, Enum):
class MessageModel(BaseModel):
- msgtype: str = None
- text: Optional[TextMessage] = None
- msgId: str
+ chatbotUserId: str = None
+ conversationId: str = None
+ conversationType: ConversationType = None
# ms
createAt: int = None
- conversationType: ConversationType = None
- conversationId: str = None
- conversationTitle: str = None
+ isAdmin: bool = None
+ msgId: str = None
+ msgtype: str = None
+ senderCorpId: str = None
senderId: str = None
senderNick: str = None
- senderCorpId: str = None
- senderStaffId: str = None
- chatbotUserId: str = None
- chatbotCorpId: str = None
- atUsers: List[AtUsersItem] = None
sessionWebhook: str = None
# ms
sessionWebhookExpiredTime: int = None
- isAdmin: bool = None
+ text: Optional[TextMessage] = None
+
+
+class PrivateMessageModel(MessageModel):
+ chatbotCorpId: str = None
+ conversationType: ConversationType = ConversationType.private
+ senderStaffId: str = None
+
+
+class GroupMessageModel(MessageModel):
+ atUsers: List[AtUsersItem] = None
+ conversationType: ConversationType = ConversationType.group
+ conversationTitle: str = None
isInAtList: bool = None