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:
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 <y>{api}</y>")
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)

View File

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

View File

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

View File

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

View File

@ -113,7 +113,7 @@ class Matcher(metaclass=MatcherMeta):
self.state = self._default_state.copy()
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}>")
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})

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