Update ding adapter event logic

This commit is contained in:
Artin 2020-12-29 12:12:35 +08:00
parent 5028f6408a
commit 1bdbbca12f
7 changed files with 106 additions and 195 deletions

View File

@ -21,7 +21,7 @@ _✨ Python 异步机器人框架 ✨_
<a href="https://github.com/howmanybots/onebot/blob/master/README.md"> <a href="https://github.com/howmanybots/onebot/blob/master/README.md">
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="cqhttp"> <img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="cqhttp">
</a> </a>
<a href="https://ding-doc.dingtalk.com/doc#/serverapi2/krgddi"> <a href="https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p">
<img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="ding"> <img src="https://img.shields.io/badge/%E9%92%89%E9%92%89-Bot-lightgrey?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAnFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4jUzeAAAAM3RSTlMAQKSRaA+/f0YyFevh29R3cyklIfrlyrGsn41tVUs48c/HqJm9uZdhX1otGwkF9IN8V1CX0Q+IAAABY0lEQVRYw+3V2W7CMBAF0JuNQAhhX9OEfYdu9///rUVWpagE27Ef2gfO+0zGozsKnv6bMGzAhkNytIe5gDdzrwtTCwrbI8x4/NF668NAxgI3Q3UtFi3TyPwNQtPLUUmDd8YfqGLNe4v22XwEYb5zoOuF5baHq2UHtsKe5ivWfGAwrWu2mC34QM0PoCAuqZdOmiwV+5BLyMRtZ7dTSEcs48rzWfzwptMLyzpApka1SJ5FtR4kfCqNIBPEVDmqoqgwUYY5plQOlf6UEjNoOPnuKB6wzDyCrks///TDza8+PnR109WQdxLo8RKWq0PPnuXG0OXKQ6wWLFnCg75uYYbhmMIVVdQ709q33aHbGIj6Duz+2k1HQFX9VwqmY8xYsEJll2ahvhWgsjYLHFRXvIi2Qb0jzMQCzC3FAoydxCma88UCzE3JCWwkjCNYyMUCzHX4DiuTMawEwwhW6hnshPhjZzzJfAH0YacpbmRd7QAAAABJRU5ErkJggg==" alt="ding">
</a> </a>
<a href="https://core.telegram.org/bots/api"> <a href="https://core.telegram.org/bots/api">
@ -71,7 +71,7 @@ NoneBot2 的驱动框架 `Driver` 以及通信协议 `Adapter` 均可**自定义
目前 NoneBot2 内置的协议适配: 目前 NoneBot2 内置的协议适配:
- [OneBot(CQHTTP) 协议](https://github.com/howmanybots/onebot/blob/master/README.md) (QQ 等) - [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) _计划中_ - [Telegram](https://core.telegram.org/bots/api) _计划中_
## 即刻开始 ## 即刻开始

View File

@ -335,22 +335,21 @@ class Message(list, abc.ABC):
"""消息数组""" """消息数组"""
def __init__(self, def __init__(self,
message: Union[str, dict, list, T_MessageSegment, message: Union[T_MessageSegment, T_Message, Any] = None,
T_Message] = None,
*args, *args,
**kwargs): **kwargs):
""" """
:参数: :参数:
* ``message: Union[str, dict, list, MessageSegment, Message]``: 消息内容 * ``message: Union[MessageSegment, Message, Any]``: 消息内容
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if isinstance(message, (str, dict, list)): if isinstance(message, Message):
self.extend(self._construct(message))
elif isinstance(message, Message):
self.extend(message) self.extend(message)
elif isinstance(message, MessageSegment): elif isinstance(message, MessageSegment):
self.append(message) self.append(message)
else:
self.extend(self._construct(message))
def __str__(self): def __str__(self):
return ''.join((str(seg) for seg in self)) return ''.join((str(seg) for seg in self))
@ -365,9 +364,7 @@ class Message(list, abc.ABC):
@staticmethod @staticmethod
@abc.abstractmethod @abc.abstractmethod
def _construct( def _construct(msg: Union[Any]) -> Iterable[T_MessageSegment]:
msg: Union[str, dict, list,
BaseModel]) -> Iterable[T_MessageSegment]:
raise NotImplementedError raise NotImplementedError
def __add__(self: T_Message, other: Union[str, T_MessageSegment, def __add__(self: T_Message, other: Union[str, T_MessageSegment,

View File

@ -5,7 +5,7 @@
协议详情请看: `钉钉文档`_ 协议详情请看: `钉钉文档`_
.. _钉钉文档: .. _钉钉文档:
https://ding-doc.dingtalk.com/doc#/serverapi2/krgddi https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p/
""" """

View File

@ -11,8 +11,8 @@ from nonebot.adapters import Bot as BaseBot
from nonebot.exception import RequestDenied from nonebot.exception import RequestDenied
from .utils import log from .utils import log
from .event import Event from .event import Event, MessageEvent, PrivateMessageEvent, GroupMessageEvent
from .model import MessageModel from .model import ConversationType
from .message import Message, MessageSegment from .message import Message, MessageSegment
from .exception import NetworkError, ApiNotAvailable, ActionFailed, SessionExpired from .exception import NetworkError, ApiNotAvailable, ActionFailed, SessionExpired
@ -50,7 +50,8 @@ class Bot(BaseBot):
# 检查连接方式 # 检查连接方式
if connection_type not in ["http"]: if connection_type not in ["http"]:
raise RequestDenied(405, "Unsupported connection type") raise RequestDenied(
405, "Unsupported connection type, available type: `http`")
# 检查 timestamp # 检查 timestamp
if not timestamp: if not timestamp:
@ -73,22 +74,30 @@ class Bot(BaseBot):
return body["chatbotUserId"] return body["chatbotUserId"]
async def handle_message(self, body: dict): async def handle_message(self, body: dict):
message = MessageModel.parse_obj(body) if not body:
if not message: 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 return
try: try:
event = Event(message)
await handle_event(self, event) await handle_event(self, event)
except Exception as e: except Exception as e:
logger.opt(colors=True, exception=e).error( logger.opt(colors=True, exception=e).error(
f"<r><bg #f8bbd0>Failed to handle event. Raw: {message}</bg #f8bbd0></r>" f"<r><bg #f8bbd0>Failed to handle event. Raw: {event}</bg #f8bbd0></r>"
) )
return return
async def call_api(self, async def call_api(self,
api: str, api: str,
event: Optional[Event] = None, event: Optional[MessageEvent] = None,
**data) -> Any: **data) -> Any:
""" """
:说明: :说明:
@ -124,10 +133,10 @@ class Bot(BaseBot):
if event: if event:
# 确保 sessionWebhook 没有过期 # 确保 sessionWebhook 没有过期
if int(datetime.now().timestamp()) > int( if int(datetime.now().timestamp()) > int(
event.raw_event.sessionWebhookExpiredTime / 1000): event.sessionWebhookExpiredTime / 1000):
raise SessionExpired raise SessionExpired
target = event.raw_event.sessionWebhook target = event.sessionWebhook
else: else:
target = None target = None

View File

@ -1,197 +1,84 @@
from typing import Union, Optional 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.adapters import Event as BaseEvent
from nonebot.utils import escape_tag
from .message import Message from .message import Message
from .model import MessageModel, ConversationType, TextMessage from .model import MessageModel, PrivateMessageModel, GroupMessageModel, ConversationType, TextMessage
class Event(BaseEvent): class Event(BaseEvent):
""" """
钉钉 协议 Event 适配继承属性参考 `BaseEvent <./#class-baseevent>`_ 。 钉钉 协议 Event 适配继承属性参考 `BaseEvent <./#class-baseevent>`_ 。
""" """
message: Message = None
def __init__(self, message: MessageModel): def __init__(self, **data):
super().__init__(message) super().__init__(**data)
# 其实目前钉钉机器人只能接收到 text 类型的消息 # 其实目前钉钉机器人只能接收到 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 get_type(self) -> Literal["message"]:
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:
""" """
- 类型: ``str`` - 类型: ``str``
- 说明: 事件类型 - 说明: 事件类型
""" """
return "message" return "message"
@type.setter def get_event_name(self) -> str:
def type(self, value) -> None: detail_type = self.conversationType.name
pass return self.get_type() + "." + detail_type
@property def get_event_description(self) -> str:
def detail_type(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"<le>{escape_tag(str(x))}</le>",
self.message,
)) + '"')
def get_user_id(self) -> str:
return self.senderId
def get_session_id(self) -> str:
""" """
- 类型: ``str`` - 类型: ``str``
- 说明: 事件详细类型 - 说明: 消息 ID
""" """
return self.raw_event.conversationType.name return self.msgId
@detail_type.setter def get_message(self) -> "Message":
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:
""" """
- 类型: ``None`` - 类型: ``Message``
- 说明: 钉钉适配器无事件子类型
"""
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]``
- 说明: 消息内容 - 说明: 消息内容
""" """
return self._message return self.message
@message.setter def get_plaintext(self) -> str:
def message(self, value) -> None:
self._message = value
@property
def reply(self) -> None:
""" """
- 类型: ``None`` - 类型: ``str``
- 说明: 回复消息详情
"""
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]``
- 说明: 纯文本消息内容 - 说明: 纯文本消息内容
""" """
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 class MessageEvent(MessageModel, Event):
def sender(self, value) -> None: pass
def set_wrapper(name):
if value.get(name):
setattr(self.raw_event, name, value.get(name))
set_wrapper("senderId") class PrivateMessageEvent(PrivateMessageModel, Event):
set_wrapper("senderNick")
set_wrapper("senderCorpId") def is_tome(self) -> bool:
set_wrapper("senderStaffId") return True
class GroupMessageEvent(GroupMessageModel, Event):
def is_tome(self) -> bool:
return self.isInAtList

View File

@ -37,6 +37,12 @@ class MessageSegment(BaseMessageSegment):
return MessageSegment.from_segment(self) return MessageSegment.from_segment(self)
return Message(self) + other 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): def atMobile(self, mobileNumber):
self.data.setdefault("at", {}) self.data.setdefault("at", {})
self.data["at"].setdefault("atMobiles", []) self.data["at"].setdefault("atMobiles", [])
@ -118,6 +124,10 @@ class Message(BaseMessage):
钉钉 协议 Message 适配 钉钉 协议 Message 适配
""" """
@classmethod
def _validate(cls, value):
return cls(value)
@staticmethod @staticmethod
def _construct( def _construct(
msg: Union[str, dict, list, msg: Union[str, dict, list,

View File

@ -26,23 +26,31 @@ class ConversationType(str, Enum):
class MessageModel(BaseModel): class MessageModel(BaseModel):
msgtype: str = None chatbotUserId: str = None
text: Optional[TextMessage] = None conversationId: str = None
msgId: str conversationType: ConversationType = None
# ms # ms
createAt: int = None createAt: int = None
conversationType: ConversationType = None isAdmin: bool = None
conversationId: str = None msgId: str = None
conversationTitle: str = None msgtype: str = None
senderCorpId: str = None
senderId: str = None senderId: str = None
senderNick: str = None senderNick: str = None
senderCorpId: str = None
senderStaffId: str = None
chatbotUserId: str = None
chatbotCorpId: str = None
atUsers: List[AtUsersItem] = None
sessionWebhook: str = None sessionWebhook: str = None
# ms # ms
sessionWebhookExpiredTime: int = None 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 isInAtList: bool = None