improve ding adapter

add tests/test_ding.py
add some log
This commit is contained in:
Artin 2020-12-30 18:33:54 +08:00
parent 3cb2b44130
commit 086a998b20
7 changed files with 241 additions and 53 deletions

View File

@ -19,6 +19,8 @@ from .event import Event, MessageEvent, PrivateMessageEvent, GroupMessageEvent,
if TYPE_CHECKING: if TYPE_CHECKING:
from nonebot.drivers import Driver from nonebot.drivers import Driver
SEND_BY_SESSION_WEBHOOK = "send_by_sessionWebhook"
class Bot(BaseBot): class Bot(BaseBot):
""" """
@ -89,7 +91,7 @@ class Bot(BaseBot):
else: else:
raise ValueError("Unsupported conversation type") raise ValueError("Unsupported conversation type")
except Exception as e: except Exception as e:
log("Error", "Event Parser Error", e) log("ERROR", "Event Parser Error", e)
return return
try: try:
@ -135,7 +137,7 @@ class Bot(BaseBot):
log("DEBUG", f"Calling API <y>{api}</y>") log("DEBUG", f"Calling API <y>{api}</y>")
if api == "send_message": if api == SEND_BY_SESSION_WEBHOOK:
if event: if event:
# 确保 sessionWebhook 没有过期 # 确保 sessionWebhook 没有过期
if int(datetime.now().timestamp()) > int( if int(datetime.now().timestamp()) > int(
@ -208,10 +210,8 @@ class Bot(BaseBot):
params.update(kwargs) params.update(kwargs)
if at_sender and event.conversationType != ConversationType.private: if at_sender and event.conversationType != ConversationType.private:
params[ params["message"] = f"@{event.senderNick} " + msg
"message"] = f"@{event.senderId} " + msg + MessageSegment.atMobiles(
event.senderId)
else: else:
params["message"] = msg params["message"] = msg
return await self.call_api("send_message", **params) return await self.call_api(SEND_BY_SESSION_WEBHOOK, **params)

View File

@ -2,9 +2,8 @@ from enum import Enum
from typing import List, Optional from typing import List, Optional
from typing_extensions import Literal 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.typing import overrides
from nonebot.adapters import Event as BaseEvent from nonebot.adapters import Event as BaseEvent
@ -27,27 +26,27 @@ class Event(BaseEvent):
@overrides(BaseEvent) @overrides(BaseEvent)
def get_event_name(self) -> str: def get_event_name(self) -> str:
raise ValueError("Event has no type!") raise ValueError("Event has no name!")
@overrides(BaseEvent) @overrides(BaseEvent)
def get_event_description(self) -> str: def get_event_description(self) -> str:
raise ValueError("Event has no type!") raise ValueError("Event has no description!")
@overrides(BaseEvent) @overrides(BaseEvent)
def get_message(self) -> "Message": def get_message(self) -> "Message":
raise ValueError("Event has no type!") raise ValueError("Event has no message!")
@overrides(BaseEvent) @overrides(BaseEvent)
def get_plaintext(self) -> str: def get_plaintext(self) -> str:
raise ValueError("Event has no type!") raise ValueError("Event has no plaintext!")
@overrides(BaseEvent) @overrides(BaseEvent)
def get_user_id(self) -> str: def get_user_id(self) -> str:
raise ValueError("Event has no type!") raise ValueError("Event has no user_id!")
@overrides(BaseEvent) @overrides(BaseEvent)
def get_session_id(self) -> str: def get_session_id(self) -> str:
raise ValueError("Event has no type!") raise ValueError("Event has no session_id!")
@overrides(BaseEvent) @overrides(BaseEvent)
def is_tome(self) -> bool: def is_tome(self) -> bool:
@ -82,6 +81,21 @@ class MessageEvent(Event):
sessionWebhookExpiredTime: int sessionWebhookExpiredTime: int
isAdmin: bool 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) @overrides(Event)
def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: def get_type(self) -> Literal["message", "notice", "request", "meta_event"]:
return "message" return "message"
@ -94,6 +108,10 @@ class MessageEvent(Event):
def get_event_description(self) -> str: def get_event_description(self) -> str:
return f'Message[{self.msgtype}] {self.msgId} from {self.senderId} "{self.text.content}"' 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) @overrides(BaseEvent)
def get_plaintext(self) -> str: def get_plaintext(self) -> str:
return self.text.content return self.text.content

View File

@ -37,7 +37,7 @@ class ActionFailed(BaseActionFailed, DingAdapterException):
self.errmsg = errmsg self.errmsg = errmsg
def __repr__(self): def __repr__(self):
return f"<ApiError errcode={self.errcode} errmsg={self.errmsg}>" return f"<ApiError errcode={self.errcode} errmsg=\"{self.errmsg}\">"
def __str__(self): def __str__(self):
return self.__repr__() return self.__repr__()

View File

@ -1,7 +1,8 @@
from typing import Any, Dict, Union, Iterable from typing import Any, Dict, Union, Iterable
from nonebot.adapters import Message as BaseMessage, MessageSegment as BaseMessageSegment from nonebot.adapters import Message as BaseMessage, MessageSegment as BaseMessageSegment
from copy import copy
class MessageSegment(BaseMessageSegment): class MessageSegment(BaseMessageSegment):
""" """
@ -39,6 +40,16 @@ class MessageSegment(BaseMessageSegment):
def text(text: str) -> "MessageSegment": def text(text: str) -> "MessageSegment":
return MessageSegment("text", {"content": text}) 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 @staticmethod
def markdown(title: str, text: str) -> "MessageSegment": def markdown(title: str, text: str) -> "MessageSegment":
return MessageSegment( return MessageSegment(
@ -50,21 +61,21 @@ class MessageSegment(BaseMessageSegment):
) )
@staticmethod @staticmethod
def actionCardSingleBtn(title: str, text: str, btnTitle: str, def actionCardSingleBtn(title: str, text: str, singleTitle: str,
btnUrl) -> "MessageSegment": singleURL) -> "MessageSegment":
return MessageSegment( return MessageSegment(
"actionCard", { "actionCard", {
"title": title, "title": title,
"text": text, "text": text,
"singleTitle": btnTitle, "singleTitle": singleTitle,
"singleURL": btnUrl "singleURL": singleURL
}) })
@staticmethod @staticmethod
def actionCardMultiBtns( def actionCardMultiBtns(
title: str, title: str,
text: str, text: str,
btns: list = [], btns: list,
hideAvatar: bool = False, hideAvatar: bool = False,
btnOrientation: str = '1', btnOrientation: str = '1',
) -> "MessageSegment": ) -> "MessageSegment":
@ -85,7 +96,7 @@ class MessageSegment(BaseMessageSegment):
}) })
@staticmethod @staticmethod
def feedCard(links: list = []) -> "MessageSegment": def feedCard(links: list) -> "MessageSegment":
""" """
:参数: :参数:
@ -94,9 +105,19 @@ class MessageSegment(BaseMessageSegment):
return MessageSegment("feedCard", {"links": links}) return MessageSegment("feedCard", {"links": links})
@staticmethod @staticmethod
def empty() -> "MessageSegment": def raw(data) -> "MessageSegment":
"""不想回复消息到群里""" return MessageSegment('raw', data)
return MessageSegment("empty", {})
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): class Message(BaseMessage):
@ -104,10 +125,6 @@ class Message(BaseMessage):
钉钉 协议 Message 适配 钉钉 协议 Message 适配
""" """
@classmethod
def _validate(cls, value):
return cls(value)
@staticmethod @staticmethod
def _construct(msg: Union[str, dict, list]) -> Iterable[MessageSegment]: def _construct(msg: Union[str, dict, list]) -> Iterable[MessageSegment]:
if isinstance(msg, dict): if isinstance(msg, dict):
@ -121,23 +138,11 @@ class Message(BaseMessage):
def _produce(self) -> dict: def _produce(self) -> dict:
data = {} data = {}
for segment in self: for segment in self:
if segment.type == "text": # text 可以和 text 合并
data["msgtype"] = "text" if segment.type == "text" and data.get("msgtype") == 'text':
data.setdefault("text", {}) data.setdefault("text", {})
data["text"]["content"] = data["text"].setdefault( data["text"]["content"] = data["text"].setdefault(
"content", "") + segment.data["content"] "content", "") + segment.data["content"]
elif segment.type == "markdown": else:
data["msgtype"] = "markdown" data.update(segment.to_dict())
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 return data

View File

@ -113,7 +113,7 @@ class Matcher(metaclass=MatcherMeta):
self.state = self._default_state.copy() self.state = self._default_state.copy()
def __repr__(self) -> str: def __repr__(self) -> str:
return (f"<Matcher from {self.module or 'unknow'}, type={self.type}, " return (f"<Matcher from {self.module or 'unknown'}, type={self.type}, "
f"priority={self.priority}, temp={self.temp}>") f"priority={self.priority}, temp={self.temp}>")
def __str__(self) -> str: def __str__(self) -> str:
@ -460,13 +460,23 @@ class Matcher(metaclass=MatcherMeta):
if not hasattr(handler, "__params__"): if not hasattr(handler, "__params__"):
self.process_handler(handler) self.process_handler(handler)
params = getattr(handler, "__params__") params = getattr(handler, "__params__")
BotType = ((params["bot"] is not inspect.Parameter.empty) and BotType = ((params["bot"] is not inspect.Parameter.empty) and
inspect.isclass(params["bot"]) and params["bot"]) 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 EventType = ((params["event"] is not inspect.Parameter.empty) and
inspect.isclass(params["event"]) and params["event"]) inspect.isclass(params["event"]) and params["event"])
if (BotType and not isinstance(bot, BotType)) or ( if EventType and not isinstance(event, EventType):
EventType and not isinstance(event, EventType)): logger.info(
f"Matcher {self} event type {type(event)} not match annotation {EventType}, ignored"
)
return return
args = {"bot": bot, "event": event, "state": state, "matcher": self} args = {"bot": bot, "event": event, "state": state, "matcher": self}
await handler( await handler(
**{k: v for k, v in args.items() if params[k] is not None}) **{k: v for k, v in args.items() if params[k] is not None})

View File

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

View File

@ -11,8 +11,3 @@ test_command = on_startswith("hello", to_me(), permission=SUPERUSER)
@test_command.handle() @test_command.handle()
async def test_handler(bot: CQHTTPBot): async def test_handler(bot: CQHTTPBot):
await test_command.finish("cqhttp hello") await test_command.finish("cqhttp hello")
@test_command.handle()
async def test_handler(bot: DingBot):
await test_command.finish("ding hello")