diff --git a/README.md b/README.md index 21f6276d..10633bdd 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ _✨ Python 异步机器人框架 ✨_ cqhttp - + ding @@ -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