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