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">
<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 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">
</a>
<a href="https://core.telegram.org/bots/api">
@ -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) _计划中_
## 即刻开始

View File

@ -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,

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 .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"<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
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

View File

@ -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"<le>{escape_tag(str(x))}</le>",
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

View File

@ -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,

View File

@ -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