From 1bdbbca12fd4744b9d9092cf3c189e169fc92859 Mon Sep 17 00:00:00 2001 From: Artin Date: Tue, 29 Dec 2020 12:12:35 +0800 Subject: [PATCH 01/12] :sparkles: Update ding adapter event logic --- README.md | 4 +- nonebot/adapters/__init__.py | 15 +-- nonebot/adapters/ding/__init__.py | 2 +- nonebot/adapters/ding/bot.py | 29 +++-- nonebot/adapters/ding/event.py | 209 +++++++----------------------- nonebot/adapters/ding/message.py | 10 ++ nonebot/adapters/ding/model.py | 32 +++-- 7 files changed, 106 insertions(+), 195 deletions(-) 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 From f4ba97ebab4a914fcf6ca5d10dbff2de5d63fb59 Mon Sep 17 00:00:00 2001 From: nonebot Date: Tue, 29 Dec 2020 04:20:09 +0000 Subject: [PATCH 02/12] :memo: update api docs --- docs/api/adapters/README.md | 2 +- docs/api/adapters/ding.md | 201 +++++++++++++++++------------------- 2 files changed, 97 insertions(+), 106 deletions(-) diff --git a/docs/api/adapters/README.md b/docs/api/adapters/README.md index 56319168..558a062e 100644 --- a/docs/api/adapters/README.md +++ b/docs/api/adapters/README.md @@ -370,7 +370,7 @@ Event 基类。提供获取关键信息的方法,其余信息可直接获取 * **参数** - * `message: Union[str, dict, list, BaseModel, MessageSegment, Message]`: 消息内容 + * `message: Union[MessageSegment, Message, Any]`: 消息内容 diff --git a/docs/api/adapters/ding.md b/docs/api/adapters/ding.md index 8089d938..d1ecbe2b 100644 --- a/docs/api/adapters/ding.md +++ b/docs/api/adapters/ding.md @@ -198,48 +198,7 @@ sidebarDepth: 0 钉钉 协议 Event 适配。继承属性参考 [BaseEvent](./#class-baseevent) 。 -### _property_ `raw_event` - -原始上报消息 - - -### _property_ `id` - - -* 类型: `Optional[str]` - - -* 说明: 消息 ID - - -### _property_ `name` - - -* 类型: `str` - - -* 说明: 事件名称,由 type.\`detail_type\` 组合而成 - - -### _property_ `self_id` - - -* 类型: `str` - - -* 说明: 机器人自身 ID - - -### _property_ `time` - - -* 类型: `int` - - -* 说明: 消息的时间戳,单位 s - - -### _property_ `type` +### `get_type()` * 类型: `str` @@ -248,94 +207,126 @@ sidebarDepth: 0 * 说明: 事件类型 -### _property_ `detail_type` +### `get_event_name()` + + +* **说明** + + 获取事件名称的方法。 + + + +* **返回** + + + * `str` + + + +### `get_event_description()` + + +* **说明** + + 获取事件描述的方法,通常为事件具体内容。 + + + +* **返回** + + + * `str` + + + +### `get_user_id()` + + +* **说明** + + 获取事件主体 id 的方法,通常是用户 id 。 + + + +* **返回** + + + * `str` + + + +### `get_session_id()` * 类型: `str` -* 说明: 事件详细类型 +* 说明: 消息 ID -### _property_ `sub_type` +### `get_message()` -* 类型: `None` - - -* 说明: 钉钉适配器无事件子类型 - - -### _property_ `user_id` - - -* 类型: `Optional[str]` - - -* 说明: 发送者 ID - - -### _property_ `group_id` - - -* 类型: `Optional[str]` - - -* 说明: 事件主体群 ID - - -### _property_ `to_me` - - -* 类型: `Optional[bool]` - - -* 说明: 消息是否与机器人相关 - - -### _property_ `message` - - -* 类型: `Optional[Message]` +* 类型: `Message` * 说明: 消息内容 -### _property_ `reply` +### `get_plaintext()` -* 类型: `None` - - -* 说明: 回复消息详情 - - -### _property_ `raw_message` - - -* 类型: `Optional[str]` - - -* 说明: 原始消息 - - -### _property_ `plain_text` - - -* 类型: `Optional[str]` +* 类型: `str` * 说明: 纯文本消息内容 -### _property_ `sender` +## _class_ `MessageEvent` + +基类:`nonebot.adapters.ding.model.MessageModel`, `nonebot.adapters.ding.event.Event` -* 类型: `Optional[dict]` +## _class_ `PrivateMessageEvent` + +基类:`nonebot.adapters.ding.model.PrivateMessageModel`, `nonebot.adapters.ding.event.Event` -* 说明: 消息发送者信息 +### `is_tome()` + + +* **说明** + + 获取事件是否与机器人有关的方法。 + + + +* **返回** + + + * `bool` + + + +## _class_ `GroupMessageEvent` + +基类:`nonebot.adapters.ding.model.GroupMessageModel`, `nonebot.adapters.ding.event.Event` + + +### `is_tome()` + + +* **说明** + + 获取事件是否与机器人有关的方法。 + + + +* **返回** + + + * `bool` + ## _class_ `MessageSegment` From f13befc4d88702686fade55d24d45d16c23c36bc Mon Sep 17 00:00:00 2001 From: Artin Date: Tue, 29 Dec 2020 14:50:07 +0800 Subject: [PATCH 03/12] Update nonebot/adapters/__init__.py Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> --- nonebot/adapters/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index aca5ab8e..a8de2e27 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -335,7 +335,7 @@ class Message(list, abc.ABC): """消息数组""" def __init__(self, - message: Union[T_MessageSegment, T_Message, Any] = None, + message: Union[str, list, dict, T_MessageSegment, T_Message, Any] = None, *args, **kwargs): """ From 13f6a692a70760427d8aa2f621552cb61d43f3d9 Mon Sep 17 00:00:00 2001 From: Artin Date: Tue, 29 Dec 2020 14:50:15 +0800 Subject: [PATCH 04/12] Update nonebot/adapters/__init__.py Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> --- nonebot/adapters/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index a8de2e27..1d874911 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -364,7 +364,7 @@ class Message(list, abc.ABC): @staticmethod @abc.abstractmethod - def _construct(msg: Union[Any]) -> Iterable[T_MessageSegment]: + def _construct(msg: Union[str, list, dict, Any]) -> Iterable[T_MessageSegment]: raise NotImplementedError def __add__(self: T_Message, other: Union[str, T_MessageSegment, From a5947922b6fdd725b3b76a20766b1087145c1b58 Mon Sep 17 00:00:00 2001 From: Artin Date: Tue, 29 Dec 2020 14:50:24 +0800 Subject: [PATCH 05/12] Update nonebot/adapters/__init__.py Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> --- nonebot/adapters/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index 1d874911..81d2a1fb 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -341,7 +341,7 @@ class Message(list, abc.ABC): """ :参数: - * ``message: Union[MessageSegment, Message, Any]``: 消息内容 + * ``message: Union[str, list, dict, MessageSegment, Message, Any]``: 消息内容 """ super().__init__(*args, **kwargs) if isinstance(message, Message): From 0221d02ca7abf945ae26ce3dce3116cbb0bb7bc6 Mon Sep 17 00:00:00 2001 From: nonebot Date: Tue, 29 Dec 2020 06:51:55 +0000 Subject: [PATCH 06/12] :memo: update api docs --- docs/api/adapters/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/adapters/README.md b/docs/api/adapters/README.md index 558a062e..2d466856 100644 --- a/docs/api/adapters/README.md +++ b/docs/api/adapters/README.md @@ -370,7 +370,7 @@ Event 基类。提供获取关键信息的方法,其余信息可直接获取 * **参数** - * `message: Union[MessageSegment, Message, Any]`: 消息内容 + * `message: Union[str, list, dict, MessageSegment, Message, Any]`: 消息内容 From c8cd6de2f20b979a6f9854baec717e97c8b23bd7 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Wed, 30 Dec 2020 00:36:29 +0800 Subject: [PATCH 07/12] :zap: improve ding adapter --- nonebot/adapters/__init__.py | 12 ++- nonebot/adapters/cqhttp/bot.py | 1 - nonebot/adapters/ding/__init__.py | 2 +- nonebot/adapters/ding/bot.py | 50 +++++----- nonebot/adapters/ding/event.py | 146 ++++++++++++++++++----------- nonebot/adapters/ding/exception.py | 8 +- nonebot/adapters/ding/message.py | 112 +++++++++++----------- nonebot/adapters/ding/model.py | 56 ----------- 8 files changed, 194 insertions(+), 193 deletions(-) delete mode 100644 nonebot/adapters/ding/model.py diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index 81d2a1fb..5f630bdf 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -6,6 +6,7 @@ """ import abc +from copy import copy from typing_extensions import Literal from functools import reduce, partial from dataclasses import dataclass, field @@ -292,7 +293,7 @@ class MessageSegment(abc.ABC): @abc.abstractmethod def __add__(self: T_MessageSegment, other: Union[str, T_MessageSegment, - T_Message]) -> "T_Message": + T_Message]) -> T_Message: """你需要在这里实现不同消息段的合并: 比如: if isinstance(other, str): @@ -326,6 +327,9 @@ class MessageSegment(abc.ABC): def get(self, key, default=None): return getattr(self, key, default) + def copy(self: T_MessageSegment) -> T_MessageSegment: + return copy(self) + @abc.abstractmethod def is_text(self) -> bool: raise NotImplementedError @@ -335,7 +339,8 @@ class Message(list, abc.ABC): """消息数组""" def __init__(self, - message: Union[str, list, dict, T_MessageSegment, T_Message, Any] = None, + message: Union[str, list, dict, T_MessageSegment, T_Message, + Any] = None, *args, **kwargs): """ @@ -364,7 +369,8 @@ class Message(list, abc.ABC): @staticmethod @abc.abstractmethod - def _construct(msg: Union[str, list, dict, Any]) -> Iterable[T_MessageSegment]: + def _construct( + msg: Union[str, list, dict, Any]) -> Iterable[T_MessageSegment]: raise NotImplementedError def __add__(self: T_Message, other: Union[str, T_MessageSegment, diff --git a/nonebot/adapters/cqhttp/bot.py b/nonebot/adapters/cqhttp/bot.py index ee29e903..aa2783f3 100644 --- a/nonebot/adapters/cqhttp/bot.py +++ b/nonebot/adapters/cqhttp/bot.py @@ -6,7 +6,6 @@ import asyncio from typing import Any, Dict, Union, Optional, TYPE_CHECKING import httpx - from nonebot.log import logger from nonebot.config import Config from nonebot.typing import overrides diff --git a/nonebot/adapters/ding/__init__.py b/nonebot/adapters/ding/__init__.py index ea076c6f..b27d9c3e 100644 --- a/nonebot/adapters/ding/__init__.py +++ b/nonebot/adapters/ding/__init__.py @@ -11,7 +11,7 @@ from .utils import log from .bot import Bot -from .event import Event from .message import Message, MessageSegment +from .event import Event, MessageEvent, PrivateMessageEvent, GroupMessageEvent from .exception import (DingAdapterException, ApiNotAvailable, NetworkError, ActionFailed, SessionExpired) diff --git a/nonebot/adapters/ding/bot.py b/nonebot/adapters/ding/bot.py index 18dc5b69..eddccd33 100644 --- a/nonebot/adapters/ding/bot.py +++ b/nonebot/adapters/ding/bot.py @@ -6,18 +6,18 @@ from typing import Any, Union, Optional, TYPE_CHECKING import httpx from nonebot.log import logger from nonebot.config import Config +from nonebot.typing import overrides from nonebot.message import handle_event from nonebot.adapters import Bot as BaseBot from nonebot.exception import RequestDenied from .utils import log -from .event import Event, MessageEvent, PrivateMessageEvent, GroupMessageEvent -from .model import ConversationType from .message import Message, MessageSegment from .exception import NetworkError, ApiNotAvailable, ActionFailed, SessionExpired +from .event import Event, MessageEvent, PrivateMessageEvent, GroupMessageEvent, ConversationType if TYPE_CHECKING: - from nonebot.drivers import BaseDriver as Driver + from nonebot.drivers import Driver class Bot(BaseBot): @@ -38,6 +38,7 @@ class Bot(BaseBot): return "ding" @classmethod + @overrides(BaseBot) async def check_permission(cls, driver: "Driver", connection_type: str, headers: dict, body: Optional[dict]) -> str: """ @@ -73,18 +74,22 @@ class Bot(BaseBot): log("WARNING", "Ding signature check ignored!") return body["chatbotUserId"] - async def handle_message(self, body: dict): - if not body: + @overrides(BaseBot) + async def handle_message(self, message: dict): + 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: + try: + conversation_type = message["conversationType"] + if conversation_type == ConversationType.private: + event = PrivateMessageEvent.parse_obj(message) + elif conversation_type == ConversationType.group: + event = GroupMessageEvent.parse_obj(message) + else: + raise ValueError("Unsupported conversation type") + except Exception as e: + log("Error", "Event Parser Error", e) return try: @@ -95,6 +100,7 @@ class Bot(BaseBot): ) return + @overrides(BaseBot) async def call_api(self, api: str, event: Optional[MessageEvent] = None, @@ -138,19 +144,18 @@ class Bot(BaseBot): target = event.sessionWebhook else: - target = None - - if not target: raise ApiNotAvailable headers = {} - segment: MessageSegment = data["message"][0] + message: Message = data.get("message", None) + if not message: + raise ValueError("Message not found") try: async with httpx.AsyncClient(headers=headers) as client: response = await client.post( target, params={"access_token": self.config.access_token}, - json=segment.data, + json=message._produce(), timeout=self.config.api_timeout) if 200 <= response.status_code < 300: @@ -167,8 +172,9 @@ class Bot(BaseBot): except httpx.HTTPError: raise NetworkError("HTTP request failed") + @overrides(BaseBot) async def send(self, - event: Event, + event: MessageEvent, message: Union[str, "Message", "MessageSegment"], at_sender: bool = False, **kwargs) -> Any: @@ -196,13 +202,15 @@ class Bot(BaseBot): """ msg = message if isinstance(message, Message) else Message(message) - at_sender = at_sender and bool(event.user_id) + at_sender = at_sender and bool(event.senderId) params = {} params["event"] = event params.update(kwargs) - if at_sender and event.detail_type != "private": - params["message"] = f"@{event.user_id} " + msg + if at_sender and event.conversationType != ConversationType.private: + params[ + "message"] = f"@{event.senderId} " + msg + MessageSegment.atMobiles( + event.senderId) else: params["message"] = msg diff --git a/nonebot/adapters/ding/event.py b/nonebot/adapters/ding/event.py index 507e9ccc..d5c670e5 100644 --- a/nonebot/adapters/ding/event.py +++ b/nonebot/adapters/ding/event.py @@ -1,84 +1,124 @@ -from typing import Union, Optional +from enum import Enum +from typing import List, Optional from typing_extensions import Literal -from pydantic import BaseModel, validator, parse_obj_as -from pydantic.fields import ModelField +from pydantic import BaseModel -from nonebot.adapters import Event as BaseEvent from nonebot.utils import escape_tag +from nonebot.typing import overrides +from nonebot.adapters import Event as BaseEvent from .message import Message -from .model import MessageModel, PrivateMessageModel, GroupMessageModel, ConversationType, TextMessage class Event(BaseEvent): """ - 钉钉 协议 Event 适配。继承属性参考 `BaseEvent <./#class-baseevent>`_ 。 + 钉钉 协议 Event 适配。各事件字段参考 `钉钉文档`_ + + .. _钉钉文档: + https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p """ - message: Message = None - def __init__(self, **data): - super().__init__(**data) - # 其实目前钉钉机器人只能接收到 text 类型的消息 - message: Union[TextMessage] = getattr(self, self.msgtype, None) - self.message = parse_obj_as(Message, message) + chatbotUserId: str - def get_type(self) -> Literal["message"]: - """ - - 类型: ``str`` - - 说明: 事件类型 - """ - return "message" + @overrides(BaseEvent) + def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: + raise ValueError("Event has no type!") + @overrides(BaseEvent) def get_event_name(self) -> str: - detail_type = self.conversationType.name - return self.get_type() + "." + detail_type + raise ValueError("Event has no type!") + @overrides(BaseEvent) 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.msgId + raise ValueError("Event has no type!") + @overrides(BaseEvent) def get_message(self) -> "Message": - """ - - 类型: ``Message`` - - 说明: 消息内容 - """ - return self.message + raise ValueError("Event has no type!") + @overrides(BaseEvent) def get_plaintext(self) -> str: - """ - - 类型: ``str`` - - 说明: 纯文本消息内容 - """ - return self.message.extract_plain_text().strip() if self.message else "" + raise ValueError("Event has no type!") + @overrides(BaseEvent) + def get_user_id(self) -> str: + raise ValueError("Event has no type!") -class MessageEvent(MessageModel, Event): - pass - - -class PrivateMessageEvent(PrivateMessageModel, Event): + @overrides(BaseEvent) + def get_session_id(self) -> str: + raise ValueError("Event has no type!") + @overrides(BaseEvent) def is_tome(self) -> bool: return True -class GroupMessageEvent(GroupMessageModel, Event): +class TextMessage(BaseModel): + content: str + +class AtUsersItem(BaseModel): + dingtalkId: str + staffId: Optional[str] + + +class ConversationType(str, Enum): + private = "1" + group = "2" + + +class MessageEvent(Event): + msgtype: str + text: TextMessage + msgId: str + createAt: int # ms + conversationType: ConversationType + conversationId: str + senderId: str + senderNick: str + senderCorpId: str + sessionWebhook: str + sessionWebhookExpiredTime: int + isAdmin: bool + + @overrides(Event) + def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: + return "message" + + @overrides(BaseEvent) + def get_event_name(self) -> str: + return f"{self.get_type()}.{self.conversationType.name}" + + @overrides(BaseEvent) + def get_event_description(self) -> str: + return f'Message[{self.msgtype}] {self.msgId} from {self.senderId} "{self.text.content}"' + + @overrides(BaseEvent) + def get_plaintext(self) -> str: + return self.text.content + + @overrides(BaseEvent) + def get_user_id(self) -> str: + return self.senderId + + @overrides(BaseEvent) + def get_session_id(self) -> str: + return self.senderId + + +class PrivateMessageEvent(MessageEvent): + chatbotCorpId: str + senderStaffId: Optional[str] + conversationType: ConversationType = ConversationType.private + + +class GroupMessageEvent(MessageEvent): + atUsers: List[AtUsersItem] + conversationType: ConversationType = ConversationType.group + conversationTitle: str + isInAtList: bool + + @overrides(MessageEvent) def is_tome(self) -> bool: return self.isInAtList diff --git a/nonebot/adapters/ding/exception.py b/nonebot/adapters/ding/exception.py index 37276eaa..63721efc 100644 --- a/nonebot/adapters/ding/exception.py +++ b/nonebot/adapters/ding/exception.py @@ -39,6 +39,9 @@ class ActionFailed(BaseActionFailed, DingAdapterException): def __repr__(self): return f"" + def __str__(self): + return self.__repr__() + class ApiNotAvailable(BaseApiNotAvailable, DingAdapterException): pass @@ -66,7 +69,7 @@ class NetworkError(BaseNetworkError, DingAdapterException): return self.__repr__() -class SessionExpired(BaseApiNotAvailable, DingAdapterException): +class SessionExpired(ApiNotAvailable, DingAdapterException): """ :说明: @@ -75,3 +78,6 @@ class SessionExpired(BaseApiNotAvailable, DingAdapterException): def __repr__(self) -> str: return f"" + + def __str__(self): + return self.__repr__() diff --git a/nonebot/adapters/ding/message.py b/nonebot/adapters/ding/message.py index cf6f56c0..db3a0083 100644 --- a/nonebot/adapters/ding/message.py +++ b/nonebot/adapters/ding/message.py @@ -2,39 +2,23 @@ from typing import Any, Dict, Union, Iterable from nonebot.adapters import Message as BaseMessage, MessageSegment as BaseMessageSegment -from .utils import log -from .model import TextMessage - class MessageSegment(BaseMessageSegment): """ 钉钉 协议 MessageSegment 适配。具体方法参考协议消息段类型或源码。 """ - def __init__(self, type_: str, msg: Dict[str, Any]) -> None: - data = { - "msgtype": type_, - } - if msg: - data.update(msg) - log("DEBUG", f"data {data}") + def __init__(self, type_: str, data: Dict[str, Any]) -> None: super().__init__(type=type_, data=data) - @classmethod - def from_segment(cls, segment: "MessageSegment"): - return MessageSegment(segment.type, segment.data) - def __str__(self): - log("DEBUG", f"__str__: self.type {self.type} data {self.data}") if self.type == "text": - return str(self.data["text"]["content"].strip()) + return str(self.data["content"]) + elif self.type == "markdown": + return str(self.data["text"]) return "" def __add__(self, other) -> "Message": - if isinstance(other, str): - if self.type == 'text': - self.data['text']['content'] += other - return MessageSegment.from_segment(self) return Message(self) + other def __radd__(self, other) -> "Message": @@ -43,43 +27,41 @@ class MessageSegment(BaseMessageSegment): def is_text(self) -> bool: return self.type == "text" - def atMobile(self, mobileNumber): - self.data.setdefault("at", {}) - self.data["at"].setdefault("atMobiles", []) - self.data["at"]["atMobiles"].append(mobileNumber) - - def atAll(self, value): - self.data.setdefault("at", {}) - self.data["at"]["isAtAll"] = value + @staticmethod + def atAll() -> "MessageSegment": + return MessageSegment("at", {"isAtAll": True}) @staticmethod - def text(text_: str) -> "MessageSegment": - return MessageSegment("text", {"text": {"content": text_.strip()}}) + def atMobiles(*mobileNumber: str) -> "MessageSegment": + return MessageSegment("at", {"atMobiles": list(mobileNumber)}) + + @staticmethod + def text(text: str) -> "MessageSegment": + return MessageSegment("text", {"content": text}) @staticmethod def markdown(title: str, text: str) -> "MessageSegment": - return MessageSegment("markdown", { - "markdown": { + return MessageSegment( + "markdown", + { "title": title, "text": text, }, - }) + ) @staticmethod def actionCardSingleBtn(title: str, text: str, btnTitle: str, btnUrl) -> "MessageSegment": return MessageSegment( "actionCard", { - "actionCard": { - "title": title, - "text": text, - "singleTitle": btnTitle, - "singleURL": btnUrl - } + "title": title, + "text": text, + "singleTitle": btnTitle, + "singleURL": btnUrl }) @staticmethod - def actionCardSingleMultiBtns( + def actionCardMultiBtns( title: str, text: str, btns: list = [], @@ -95,28 +77,26 @@ class MessageSegment(BaseMessageSegment): """ return MessageSegment( "actionCard", { - "actionCard": { - "title": title, - "text": text, - "hideAvatar": "1" if hideAvatar else "0", - "btnOrientation": btnOrientation, - "btns": btns - } + "title": title, + "text": text, + "hideAvatar": "1" if hideAvatar else "0", + "btnOrientation": btnOrientation, + "btns": btns }) @staticmethod - def feedCard(links: list = [],) -> "MessageSegment": + def feedCard(links: list = []) -> "MessageSegment": """ :参数: * ``links``: [{ "title": xxx, "messageURL": xxx, "picURL": xxx }, ...] """ - return MessageSegment("feedCard", {"feedCard": {"links": links}}) + return MessageSegment("feedCard", {"links": links}) @staticmethod def empty() -> "MessageSegment": """不想回复消息到群里""" - return MessageSegment("empty") + return MessageSegment("empty", {}) class Message(BaseMessage): @@ -129,17 +109,35 @@ class Message(BaseMessage): return cls(value) @staticmethod - def _construct( - msg: Union[str, dict, list, - TextMessage]) -> Iterable[MessageSegment]: + def _construct(msg: Union[str, dict, list]) -> Iterable[MessageSegment]: if isinstance(msg, dict): yield MessageSegment(msg["type"], msg.get("data") or {}) - return elif isinstance(msg, list): for seg in msg: yield MessageSegment(seg["type"], seg.get("data") or {}) - return - elif isinstance(msg, TextMessage): - yield MessageSegment("text", {"text": msg.dict()}) elif isinstance(msg, str): yield MessageSegment.text(msg) + + def _produce(self) -> dict: + data = {} + for segment in self: + if segment.type == "text": + data["msgtype"] = "text" + data.setdefault("text", {}) + data["text"]["content"] = data["text"].setdefault( + "content", "") + segment.data["content"] + elif segment.type == "markdown": + data["msgtype"] = "markdown" + data.setdefault("markdown", {}) + data["markdown"]["text"] = data["markdown"].setdefault( + "content", "") + segment.data["content"] + elif segment.type == "empty": + data["msgtype"] = "empty" + elif segment.type == "at" and "atMobiles" in segment.data: + data.setdefault("at", {}) + data["at"]["atMobiles"] = data["at"].setdefault( + "atMobiles", []) + segment.data["atMobiles"] + elif segment.data: + data.setdefault(segment.type, {}) + data[segment.type].update(segment.data) + return data diff --git a/nonebot/adapters/ding/model.py b/nonebot/adapters/ding/model.py deleted file mode 100644 index 49e4b0f5..00000000 --- a/nonebot/adapters/ding/model.py +++ /dev/null @@ -1,56 +0,0 @@ -from enum import Enum -from typing import List, Optional - -from pydantic import BaseModel - - -class Headers(BaseModel): - sign: str - token: str - # ms - timestamp: int - - -class TextMessage(BaseModel): - content: str - - -class AtUsersItem(BaseModel): - dingtalkId: str - staffId: Optional[str] - - -class ConversationType(str, Enum): - private = '1' - group = '2' - - -class MessageModel(BaseModel): - chatbotUserId: str = None - conversationId: str = None - conversationType: ConversationType = None - # ms - createAt: int = None - isAdmin: bool = None - msgId: str = None - msgtype: str = None - senderCorpId: str = None - senderId: str = None - senderNick: str = None - sessionWebhook: str = None - # ms - sessionWebhookExpiredTime: int = 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 From 3cb2b441305358fed39d77591e26844c8067146b Mon Sep 17 00:00:00 2001 From: nonebot Date: Tue, 29 Dec 2020 16:38:15 +0000 Subject: [PATCH 08/12] :memo: update api docs --- docs/api/adapters/ding.md | 204 +++++++++++++++++++++++++++++++------- 1 file changed, 170 insertions(+), 34 deletions(-) diff --git a/docs/api/adapters/ding.md b/docs/api/adapters/ding.md index d1ecbe2b..a0a724b5 100644 --- a/docs/api/adapters/ding.md +++ b/docs/api/adapters/ding.md @@ -63,7 +63,7 @@ sidebarDepth: 0 ## _exception_ `SessionExpired` -基类:[`nonebot.exception.ApiNotAvailable`](../exception.md#nonebot.exception.ApiNotAvailable), `nonebot.adapters.ding.exception.DingAdapterException` +基类:`nonebot.adapters.ding.exception.ApiNotAvailable`, `nonebot.adapters.ding.exception.DingAdapterException` * **说明** @@ -94,7 +94,7 @@ sidebarDepth: 0 -### _async_ `handle_message(body)` +### _async_ `handle_message(message)` * **说明** @@ -195,16 +195,23 @@ sidebarDepth: 0 基类:[`nonebot.adapters.Event`](README.md#nonebot.adapters.Event) -钉钉 协议 Event 适配。继承属性参考 [BaseEvent](./#class-baseevent) 。 +钉钉 协议 Event 适配。各事件字段参考 [钉钉文档](https://ding-doc.dingtalk.com/document#/org-dev-guide/elzz1p) ### `get_type()` -* 类型: `str` +* **说明** + + 获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。 -* 说明: 事件类型 + +* **返回** + + + * `Literal["message", "notice", "request", "meta_event"]` + ### `get_event_name()` @@ -239,6 +246,38 @@ sidebarDepth: 0 +### `get_message()` + + +* **说明** + + 获取事件消息内容的方法。 + + + +* **返回** + + + * `Message` + + + +### `get_plaintext()` + + +* **说明** + + 获取消息纯文本的方法,通常不需要修改,默认通过 `get_message().extract_plain_text` 获取。 + + + +* **返回** + + + * `str` + + + ### `get_user_id()` @@ -258,39 +297,18 @@ sidebarDepth: 0 ### `get_session_id()` -* 类型: `str` +* **说明** + + 获取会话 id 的方法,用于判断当前事件属于哪一个会话,通常是用户 id、群组 id 组合。 -* 说明: 消息 ID +* **返回** -### `get_message()` + + * `str` -* 类型: `Message` - - -* 说明: 消息内容 - - -### `get_plaintext()` - - -* 类型: `str` - - -* 说明: 纯文本消息内容 - - -## _class_ `MessageEvent` - -基类:`nonebot.adapters.ding.model.MessageModel`, `nonebot.adapters.ding.event.Event` - - -## _class_ `PrivateMessageEvent` - -基类:`nonebot.adapters.ding.model.PrivateMessageModel`, `nonebot.adapters.ding.event.Event` - ### `is_tome()` @@ -308,9 +326,127 @@ sidebarDepth: 0 +## _class_ `ConversationType` + +基类:`str`, `enum.Enum` + +An enumeration. + + +### `_member_type_` + +`builtins.str` 的别名 + + +## _class_ `MessageEvent` + +基类:`nonebot.adapters.ding.event.Event` + + +### `get_type()` + + +* **说明** + + 获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。 + + + +* **返回** + + + * `Literal["message", "notice", "request", "meta_event"]` + + + +### `get_event_name()` + + +* **说明** + + 获取事件名称的方法。 + + + +* **返回** + + + * `str` + + + +### `get_event_description()` + + +* **说明** + + 获取事件描述的方法,通常为事件具体内容。 + + + +* **返回** + + + * `str` + + + +### `get_plaintext()` + + +* **说明** + + 获取消息纯文本的方法,通常不需要修改,默认通过 `get_message().extract_plain_text` 获取。 + + + +* **返回** + + + * `str` + + + +### `get_user_id()` + + +* **说明** + + 获取事件主体 id 的方法,通常是用户 id 。 + + + +* **返回** + + + * `str` + + + +### `get_session_id()` + + +* **说明** + + 获取会话 id 的方法,用于判断当前事件属于哪一个会话,通常是用户 id、群组 id 组合。 + + + +* **返回** + + + * `str` + + + +## _class_ `PrivateMessageEvent` + +基类:`nonebot.adapters.ding.event.MessageEvent` + + ## _class_ `GroupMessageEvent` -基类:`nonebot.adapters.ding.model.GroupMessageModel`, `nonebot.adapters.ding.event.Event` +基类:`nonebot.adapters.ding.event.MessageEvent` ### `is_tome()` @@ -336,7 +472,7 @@ sidebarDepth: 0 钉钉 协议 MessageSegment 适配。具体方法参考协议消息段类型或源码。 -### _static_ `actionCardSingleMultiBtns(title, text, btns=[], hideAvatar=False, btnOrientation='1')` +### _static_ `actionCardMultiBtns(title, text, btns=[], hideAvatar=False, btnOrientation='1')` * **参数** From 086a998b2011be5143fcbd5b77a36c15dde73701 Mon Sep 17 00:00:00 2001 From: Artin Date: Wed, 30 Dec 2020 18:33:54 +0800 Subject: [PATCH 09/12] :zap: improve ding adapter add tests/test_ding.py add some log --- nonebot/adapters/ding/bot.py | 12 +- nonebot/adapters/ding/event.py | 34 ++++-- nonebot/adapters/ding/exception.py | 2 +- nonebot/adapters/ding/message.py | 65 ++++++----- nonebot/matcher.py | 16 ++- tests/test_plugins/test_ding.py | 160 ++++++++++++++++++++++++++ tests/test_plugins/test_permission.py | 5 - 7 files changed, 241 insertions(+), 53 deletions(-) create mode 100644 tests/test_plugins/test_ding.py diff --git a/nonebot/adapters/ding/bot.py b/nonebot/adapters/ding/bot.py index eddccd33..a736dc09 100644 --- a/nonebot/adapters/ding/bot.py +++ b/nonebot/adapters/ding/bot.py @@ -19,6 +19,8 @@ from .event import Event, MessageEvent, PrivateMessageEvent, GroupMessageEvent, if TYPE_CHECKING: from nonebot.drivers import Driver +SEND_BY_SESSION_WEBHOOK = "send_by_sessionWebhook" + class Bot(BaseBot): """ @@ -89,7 +91,7 @@ class Bot(BaseBot): else: raise ValueError("Unsupported conversation type") except Exception as e: - log("Error", "Event Parser Error", e) + log("ERROR", "Event Parser Error", e) return try: @@ -135,7 +137,7 @@ class Bot(BaseBot): log("DEBUG", f"Calling API {api}") - if api == "send_message": + if api == SEND_BY_SESSION_WEBHOOK: if event: # 确保 sessionWebhook 没有过期 if int(datetime.now().timestamp()) > int( @@ -208,10 +210,8 @@ class Bot(BaseBot): params.update(kwargs) if at_sender and event.conversationType != ConversationType.private: - params[ - "message"] = f"@{event.senderId} " + msg + MessageSegment.atMobiles( - event.senderId) + params["message"] = f"@{event.senderNick} " + msg else: params["message"] = msg - return await self.call_api("send_message", **params) + return await self.call_api(SEND_BY_SESSION_WEBHOOK, **params) diff --git a/nonebot/adapters/ding/event.py b/nonebot/adapters/ding/event.py index d5c670e5..a049a079 100644 --- a/nonebot/adapters/ding/event.py +++ b/nonebot/adapters/ding/event.py @@ -2,9 +2,8 @@ from enum import Enum from typing import List, Optional from typing_extensions import Literal -from pydantic import BaseModel +from pydantic import BaseModel, root_validator -from nonebot.utils import escape_tag from nonebot.typing import overrides from nonebot.adapters import Event as BaseEvent @@ -27,27 +26,27 @@ class Event(BaseEvent): @overrides(BaseEvent) def get_event_name(self) -> str: - raise ValueError("Event has no type!") + raise ValueError("Event has no name!") @overrides(BaseEvent) def get_event_description(self) -> str: - raise ValueError("Event has no type!") + raise ValueError("Event has no description!") @overrides(BaseEvent) def get_message(self) -> "Message": - raise ValueError("Event has no type!") + raise ValueError("Event has no message!") @overrides(BaseEvent) def get_plaintext(self) -> str: - raise ValueError("Event has no type!") + raise ValueError("Event has no plaintext!") @overrides(BaseEvent) def get_user_id(self) -> str: - raise ValueError("Event has no type!") + raise ValueError("Event has no user_id!") @overrides(BaseEvent) def get_session_id(self) -> str: - raise ValueError("Event has no type!") + raise ValueError("Event has no session_id!") @overrides(BaseEvent) def is_tome(self) -> bool: @@ -82,6 +81,21 @@ class MessageEvent(Event): sessionWebhookExpiredTime: int isAdmin: bool + message: Message + + @root_validator(pre=True) + def gen_message(cls, values: dict): + assert "msgtype" in values, "msgtype must be specified" + # 其实目前钉钉机器人只能接收到 text 类型的消息 + assert values[ + "msgtype"] in values, f"{values['msgtype']} must be specified" + content = values[values['msgtype']]['content'] + # 如果是被 @,第一个字符将会为空格,移除特殊情况 + if content[0] == ' ': + content = content[1:] + values["message"] = content + return values + @overrides(Event) def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: return "message" @@ -94,6 +108,10 @@ class MessageEvent(Event): def get_event_description(self) -> str: return f'Message[{self.msgtype}] {self.msgId} from {self.senderId} "{self.text.content}"' + @overrides(BaseEvent) + def get_message(self) -> Message: + return self.message + @overrides(BaseEvent) def get_plaintext(self) -> str: return self.text.content diff --git a/nonebot/adapters/ding/exception.py b/nonebot/adapters/ding/exception.py index 63721efc..df416932 100644 --- a/nonebot/adapters/ding/exception.py +++ b/nonebot/adapters/ding/exception.py @@ -37,7 +37,7 @@ class ActionFailed(BaseActionFailed, DingAdapterException): self.errmsg = errmsg def __repr__(self): - return f"" + return f"" def __str__(self): return self.__repr__() diff --git a/nonebot/adapters/ding/message.py b/nonebot/adapters/ding/message.py index db3a0083..ad4aa198 100644 --- a/nonebot/adapters/ding/message.py +++ b/nonebot/adapters/ding/message.py @@ -1,7 +1,8 @@ from typing import Any, Dict, Union, Iterable - from nonebot.adapters import Message as BaseMessage, MessageSegment as BaseMessageSegment +from copy import copy + class MessageSegment(BaseMessageSegment): """ @@ -39,6 +40,16 @@ class MessageSegment(BaseMessageSegment): def text(text: str) -> "MessageSegment": return MessageSegment("text", {"content": text}) + @staticmethod + def image(picURL: str) -> "MessageSegment": + return MessageSegment("image", {"picURL": picURL}) + + @staticmethod + def extension(dict_: dict) -> "MessageSegment": + """"标记 text 文本的 extension 属性,需要与 text 消息段相加。 + """ + return MessageSegment("extension", dict_) + @staticmethod def markdown(title: str, text: str) -> "MessageSegment": return MessageSegment( @@ -50,21 +61,21 @@ class MessageSegment(BaseMessageSegment): ) @staticmethod - def actionCardSingleBtn(title: str, text: str, btnTitle: str, - btnUrl) -> "MessageSegment": + def actionCardSingleBtn(title: str, text: str, singleTitle: str, + singleURL) -> "MessageSegment": return MessageSegment( "actionCard", { "title": title, "text": text, - "singleTitle": btnTitle, - "singleURL": btnUrl + "singleTitle": singleTitle, + "singleURL": singleURL }) @staticmethod def actionCardMultiBtns( title: str, text: str, - btns: list = [], + btns: list, hideAvatar: bool = False, btnOrientation: str = '1', ) -> "MessageSegment": @@ -85,7 +96,7 @@ class MessageSegment(BaseMessageSegment): }) @staticmethod - def feedCard(links: list = []) -> "MessageSegment": + def feedCard(links: list) -> "MessageSegment": """ :参数: @@ -94,9 +105,19 @@ class MessageSegment(BaseMessageSegment): return MessageSegment("feedCard", {"links": links}) @staticmethod - def empty() -> "MessageSegment": - """不想回复消息到群里""" - return MessageSegment("empty", {}) + def raw(data) -> "MessageSegment": + return MessageSegment('raw', data) + + def to_dict(self) -> dict: + # 让用户可以直接发送原始的消息格式 + if self.type == "raw": + return copy(self.data) + + # 不属于消息内容,只是作为消息段的辅助 + if self.type in ["at", "extension"]: + return {self.type: copy(self.data)} + + return {"msgtype": self.type, self.type: copy(self.data)} class Message(BaseMessage): @@ -104,10 +125,6 @@ class Message(BaseMessage): 钉钉 协议 Message 适配。 """ - @classmethod - def _validate(cls, value): - return cls(value) - @staticmethod def _construct(msg: Union[str, dict, list]) -> Iterable[MessageSegment]: if isinstance(msg, dict): @@ -121,23 +138,11 @@ class Message(BaseMessage): def _produce(self) -> dict: data = {} for segment in self: - if segment.type == "text": - data["msgtype"] = "text" + # text 可以和 text 合并 + if segment.type == "text" and data.get("msgtype") == 'text': data.setdefault("text", {}) data["text"]["content"] = data["text"].setdefault( "content", "") + segment.data["content"] - elif segment.type == "markdown": - data["msgtype"] = "markdown" - data.setdefault("markdown", {}) - data["markdown"]["text"] = data["markdown"].setdefault( - "content", "") + segment.data["content"] - elif segment.type == "empty": - data["msgtype"] = "empty" - elif segment.type == "at" and "atMobiles" in segment.data: - data.setdefault("at", {}) - data["at"]["atMobiles"] = data["at"].setdefault( - "atMobiles", []) + segment.data["atMobiles"] - elif segment.data: - data.setdefault(segment.type, {}) - data[segment.type].update(segment.data) + else: + data.update(segment.to_dict()) return data diff --git a/nonebot/matcher.py b/nonebot/matcher.py index 3b9b54c2..1045973b 100644 --- a/nonebot/matcher.py +++ b/nonebot/matcher.py @@ -113,7 +113,7 @@ class Matcher(metaclass=MatcherMeta): self.state = self._default_state.copy() def __repr__(self) -> str: - return (f"") def __str__(self) -> str: @@ -460,13 +460,23 @@ class Matcher(metaclass=MatcherMeta): if not hasattr(handler, "__params__"): self.process_handler(handler) params = getattr(handler, "__params__") + BotType = ((params["bot"] is not inspect.Parameter.empty) and inspect.isclass(params["bot"]) and params["bot"]) + if BotType and not isinstance(bot, BotType): + logger.info( + f"Matcher {self} bot type {type(bot)} not match annotation {BotType}, ignored" + ) + return + EventType = ((params["event"] is not inspect.Parameter.empty) and inspect.isclass(params["event"]) and params["event"]) - if (BotType and not isinstance(bot, BotType)) or ( - EventType and not isinstance(event, EventType)): + if EventType and not isinstance(event, EventType): + logger.info( + f"Matcher {self} event type {type(event)} not match annotation {EventType}, ignored" + ) return + args = {"bot": bot, "event": event, "state": state, "matcher": self} await handler( **{k: v for k, v in args.items() if params[k] is not None}) diff --git a/tests/test_plugins/test_ding.py b/tests/test_plugins/test_ding.py new file mode 100644 index 00000000..fca234eb --- /dev/null +++ b/tests/test_plugins/test_ding.py @@ -0,0 +1,160 @@ +from nonebot.rule import to_me +from nonebot.plugin import on_command +from nonebot.adapters.ding import Bot as DingBot, MessageSegment, MessageEvent + +markdown = on_command("markdown", to_me()) + + +@markdown.handle() +async def test_handler(bot: DingBot): + message = MessageSegment.markdown( + "Hello, This is NoneBot", + "#### NoneBot \n> Nonebot 是一款高性能的 Python 机器人框架\n> ![screenshot](https://v2.nonebot.dev/logo.png)\n> [GitHub 仓库地址](https://github.com/nonebot/nonebot2) \n" + ) + await markdown.finish(message) + + +actionCardSingleBtn = on_command("actionCardSingleBtn", to_me()) + + +@actionCardSingleBtn.handle() +async def test_handler(bot: DingBot): + message = MessageSegment.actionCardSingleBtn( + title="打造一间咖啡厅", + text= + "![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png) \n #### 乔布斯 20 年前想打造的苹果咖啡厅 \n\n Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划", + singleTitle="阅读全文", + singleURL="https://www.dingtalk.com/") + await actionCardSingleBtn.finish(message) + + +actionCard = on_command("actionCard", to_me()) + + +@actionCard.handle() +async def test_handler(bot: DingBot): + message = MessageSegment.raw({ + "msgtype": "actionCard", + "actionCard": { + "title": + "乔布斯 20 年前想打造一间苹果咖啡厅,而它正是 Apple Store 的前身", + "text": + "![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png) \n\n #### 乔布斯 20 年前想打造的苹果咖啡厅 \n\n Apple Store 的设计正从原来满满的科技感走向生活化,而其生活化的走向其实可以追溯到 20 年前苹果一个建立咖啡馆的计划", + "hideAvatar": + "0", + "btnOrientation": + "0", + "btns": [{ + "title": "内容不错", + "actionURL": "https://www.dingtalk.com/" + }, { + "title": "不感兴趣", + "actionURL": "https://www.dingtalk.com/" + }] + } + }) + await actionCard.finish(message) + + +feedCard = on_command("feedCard", to_me()) + + +@feedCard.handle() +async def test_handler(bot: DingBot): + message = MessageSegment.raw({ + "msgtype": "feedCard", + "feedCard": { + "links": [{ + "title": + "时代的火车向前开1", + "messageURL": + "https://www.dingtalk.com/", + "picURL": + "https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png" + }, { + "title": + "时代的火车向前开2", + "messageURL": + "https://www.dingtalk.com/", + "picURL": + "https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png" + }] + } + }) + await feedCard.finish(message) + + +atme = on_command("atme", to_me()) + + +@atme.handle() +async def test_handler(bot: DingBot, event: MessageEvent): + message = f"@{event.senderNick} at you" + MessageSegment.atMobiles( + "13800000001") + await atme.finish(message) + + +image = on_command("image", to_me()) + + +@image.handle() +async def test_handler(bot: DingBot, event: MessageEvent): + message = MessageSegment.image( + "https://static-aliyun-doc.oss-accelerate.aliyuncs.com/assets/img/zh-CN/0634199951/p158167.png" + ) + await image.finish(message) + + +textAdd = on_command("t", to_me()) + + +@textAdd.handle() +async def test_handler(bot: DingBot, event: MessageEvent): + message = "第一段消息\n" + MessageSegment.text("asdawefaefa\n") + await textAdd.send(message) + + message = message + MessageSegment.text("第二段消息\n") + await textAdd.send(message) + + message = message + MessageSegment.text( + "\n第三段消息\n") + "adfkasfkhsdkfahskdjasdashdkjasdf" + message = message + MessageSegment.extension({ + "text_type": "code_snippet", + "code_language": "C#" + }) + await textAdd.send(message) + + +code = on_command("code", to_me()) + + +@code.handle() +async def test_handler(bot: DingBot, event: MessageEvent): + raw = MessageSegment.raw({ + "msgtype": "text", + "text": { + "content": 'print("hello world")' + }, + "extension": { + "text_type": "code_snippet", + "code_language": "Python", + } + }) + await code.send(raw) + message = MessageSegment.text("""using System; + +namespace HelloWorld +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +}""") + message += MessageSegment.extension({ + "text_type": "code_snippet", + "code_language": "C#" + }) + await code.finish(message) diff --git a/tests/test_plugins/test_permission.py b/tests/test_plugins/test_permission.py index a7157dff..ee9a45de 100644 --- a/tests/test_plugins/test_permission.py +++ b/tests/test_plugins/test_permission.py @@ -11,8 +11,3 @@ test_command = on_startswith("hello", to_me(), permission=SUPERUSER) @test_command.handle() async def test_handler(bot: CQHTTPBot): await test_command.finish("cqhttp hello") - - -@test_command.handle() -async def test_handler(bot: DingBot): - await test_command.finish("ding hello") From 380977bc675cb13d9c54c908f7ad09e66f60f571 Mon Sep 17 00:00:00 2001 From: nonebot Date: Wed, 30 Dec 2020 10:35:51 +0000 Subject: [PATCH 10/12] :memo: update api docs --- docs/api/adapters/ding.md | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/api/adapters/ding.md b/docs/api/adapters/ding.md index a0a724b5..c22160b2 100644 --- a/docs/api/adapters/ding.md +++ b/docs/api/adapters/ding.md @@ -391,6 +391,22 @@ An enumeration. +### `get_message()` + + +* **说明** + + 获取事件消息内容的方法。 + + + +* **返回** + + + * `Message` + + + ### `get_plaintext()` @@ -472,7 +488,12 @@ An enumeration. 钉钉 协议 MessageSegment 适配。具体方法参考协议消息段类型或源码。 -### _static_ `actionCardMultiBtns(title, text, btns=[], hideAvatar=False, btnOrientation='1')` +### _static_ `extension(dict_)` + +"标记 text 文本的 extension 属性,需要与 text 消息段相加。 + + +### _static_ `actionCardMultiBtns(title, text, btns, hideAvatar=False, btnOrientation='1')` * **参数** @@ -485,7 +506,7 @@ An enumeration. -### _static_ `feedCard(links=[])` +### _static_ `feedCard(links)` * **参数** @@ -495,11 +516,6 @@ An enumeration. -### _static_ `empty()` - -不想回复消息到群里 - - ## _class_ `Message` 基类:[`nonebot.adapters.Message`](README.md#nonebot.adapters.Message) From 55f1bd1f2dcc23734c87260df7cbfad5331b9015 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Wed, 30 Dec 2020 20:08:22 +0800 Subject: [PATCH 11/12] :pencil2: update code style --- nonebot/adapters/ding/bot.py | 2 +- nonebot/adapters/ding/event.py | 12 ++++++------ nonebot/adapters/ding/message.py | 13 ++++++++++--- nonebot/matcher.py | 4 ++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/nonebot/adapters/ding/bot.py b/nonebot/adapters/ding/bot.py index a736dc09..e46febc5 100644 --- a/nonebot/adapters/ding/bot.py +++ b/nonebot/adapters/ding/bot.py @@ -98,7 +98,7 @@ class Bot(BaseBot): await handle_event(self, event) except Exception as e: logger.opt(colors=True, exception=e).error( - f"Failed to handle event. Raw: {event}" + f"Failed to handle event. Raw: {message}" ) return diff --git a/nonebot/adapters/ding/event.py b/nonebot/adapters/ding/event.py index a049a079..c9cd76dc 100644 --- a/nonebot/adapters/ding/event.py +++ b/nonebot/adapters/ding/event.py @@ -100,27 +100,27 @@ class MessageEvent(Event): def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: return "message" - @overrides(BaseEvent) + @overrides(Event) def get_event_name(self) -> str: return f"{self.get_type()}.{self.conversationType.name}" - @overrides(BaseEvent) + @overrides(Event) def get_event_description(self) -> str: return f'Message[{self.msgtype}] {self.msgId} from {self.senderId} "{self.text.content}"' - @overrides(BaseEvent) + @overrides(Event) def get_message(self) -> Message: return self.message - @overrides(BaseEvent) + @overrides(Event) def get_plaintext(self) -> str: return self.text.content - @overrides(BaseEvent) + @overrides(Event) def get_user_id(self) -> str: return self.senderId - @overrides(BaseEvent) + @overrides(Event) def get_session_id(self) -> str: return self.senderId diff --git a/nonebot/adapters/ding/message.py b/nonebot/adapters/ding/message.py index ad4aa198..2c0fa336 100644 --- a/nonebot/adapters/ding/message.py +++ b/nonebot/adapters/ding/message.py @@ -1,7 +1,8 @@ -from typing import Any, Dict, Union, Iterable -from nonebot.adapters import Message as BaseMessage, MessageSegment as BaseMessageSegment - from copy import copy +from typing import Any, Dict, Union, Iterable + +from nonebot.typing import overrides +from nonebot.adapters import Message as BaseMessage, MessageSegment as BaseMessageSegment class MessageSegment(BaseMessageSegment): @@ -9,9 +10,11 @@ class MessageSegment(BaseMessageSegment): 钉钉 协议 MessageSegment 适配。具体方法参考协议消息段类型或源码。 """ + @overrides(BaseMessageSegment) def __init__(self, type_: str, data: Dict[str, Any]) -> None: super().__init__(type=type_, data=data) + @overrides(BaseMessageSegment) def __str__(self): if self.type == "text": return str(self.data["content"]) @@ -19,12 +22,15 @@ class MessageSegment(BaseMessageSegment): return str(self.data["text"]) return "" + @overrides(BaseMessageSegment) def __add__(self, other) -> "Message": return Message(self) + other + @overrides(BaseMessageSegment) def __radd__(self, other) -> "Message": return Message(other) + self + @overrides(BaseMessageSegment) def is_text(self) -> bool: return self.type == "text" @@ -126,6 +132,7 @@ class Message(BaseMessage): """ @staticmethod + @overrides(BaseMessage) def _construct(msg: Union[str, dict, list]) -> Iterable[MessageSegment]: if isinstance(msg, dict): yield MessageSegment(msg["type"], msg.get("data") or {}) diff --git a/nonebot/matcher.py b/nonebot/matcher.py index 1045973b..ca40123d 100644 --- a/nonebot/matcher.py +++ b/nonebot/matcher.py @@ -464,7 +464,7 @@ class Matcher(metaclass=MatcherMeta): BotType = ((params["bot"] is not inspect.Parameter.empty) and inspect.isclass(params["bot"]) and params["bot"]) if BotType and not isinstance(bot, BotType): - logger.info( + logger.debug( f"Matcher {self} bot type {type(bot)} not match annotation {BotType}, ignored" ) return @@ -472,7 +472,7 @@ class Matcher(metaclass=MatcherMeta): EventType = ((params["event"] is not inspect.Parameter.empty) and inspect.isclass(params["event"]) and params["event"]) if EventType and not isinstance(event, EventType): - logger.info( + logger.debug( f"Matcher {self} event type {type(event)} not match annotation {EventType}, ignored" ) return From 1bd223eab2661297734ff55689a803f010b97205 Mon Sep 17 00:00:00 2001 From: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> Date: Wed, 30 Dec 2020 20:19:24 +0800 Subject: [PATCH 12/12] Restore test_permission.py --- tests/test_plugins/test_permission.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_plugins/test_permission.py b/tests/test_plugins/test_permission.py index ee9a45de..a7157dff 100644 --- a/tests/test_plugins/test_permission.py +++ b/tests/test_plugins/test_permission.py @@ -11,3 +11,8 @@ test_command = on_startswith("hello", to_me(), permission=SUPERUSER) @test_command.handle() async def test_handler(bot: CQHTTPBot): await test_command.finish("cqhttp hello") + + +@test_command.handle() +async def test_handler(bot: DingBot): + await test_command.finish("ding hello")