From edb4458031468d7821b83f0a83760d677261892a Mon Sep 17 00:00:00 2001 From: Artin Date: Thu, 3 Dec 2020 00:59:32 +0800 Subject: [PATCH 01/16] :sparkles: Add ding adapter --- nonebot/adapters/__init__.py | 25 ++-- nonebot/adapters/ding/__init__.py | 15 +++ nonebot/adapters/ding/bot.py | 205 ++++++++++++++++++++++++++++ nonebot/adapters/ding/event.py | 207 +++++++++++++++++++++++++++++ nonebot/adapters/ding/exception.py | 29 ++++ nonebot/adapters/ding/message.py | 133 ++++++++++++++++++ nonebot/adapters/ding/model.py | 47 +++++++ nonebot/adapters/ding/utils.py | 35 +++++ nonebot/exception.py | 6 + nonebot/typing.py | 2 +- tests/bot.py | 2 + 11 files changed, 695 insertions(+), 11 deletions(-) create mode 100644 nonebot/adapters/ding/__init__.py create mode 100644 nonebot/adapters/ding/bot.py create mode 100644 nonebot/adapters/ding/event.py create mode 100644 nonebot/adapters/ding/exception.py create mode 100644 nonebot/adapters/ding/message.py create mode 100644 nonebot/adapters/ding/model.py create mode 100644 nonebot/adapters/ding/utils.py diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index 9895b88c..a7dd7b21 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -9,9 +9,11 @@ import abc from functools import reduce, partial from dataclasses import dataclass, field +from pydantic import BaseModel + from nonebot.config import Config from nonebot.typing import Driver, Message, WebSocket -from nonebot.typing import Any, Dict, Union, Optional, NoReturn, Callable, Iterable, Awaitable +from nonebot.typing import Any, Dict, Union, Optional, NoReturn, Callable, Iterable, Awaitable, TypeVar, Generic class BaseBot(abc.ABC): @@ -135,24 +137,27 @@ class BaseBot(abc.ABC): raise NotImplementedError -class BaseEvent(abc.ABC): +T = TypeVar("T", dict, BaseModel) + + +class BaseEvent(abc.ABC, Generic[T]): """ Event 基类。提供上报信息的关键信息,其余信息可从原始上报消息获取。 """ - def __init__(self, raw_event: dict): + def __init__(self, raw_event: T): """ :参数: - * ``raw_event: dict``: 原始上报消息 + * ``raw_event: T``: 原始上报消息 """ - self._raw_event = raw_event + self._raw_event: T = raw_event def __repr__(self) -> str: return f"" @property - def raw_event(self) -> dict: + def raw_event(self) -> T: """原始上报消息""" return self._raw_event @@ -347,17 +352,17 @@ class BaseMessage(list, abc.ABC): """消息数组""" def __init__(self, - message: Union[str, dict, list, BaseMessageSegment, + message: Union[str, dict, list, BaseModel, BaseMessageSegment, "BaseMessage"] = None, *args, **kwargs): """ :参数: - * ``message: Union[str, dict, list, MessageSegment, Message]``: 消息内容 + * ``message: Union[str, dict, list, BaseModel, MessageSegment, Message]``: 消息内容 """ super().__init__(*args, **kwargs) - if isinstance(message, (str, dict, list)): + if isinstance(message, (str, dict, list, BaseModel)): self.extend(self._construct(message)) elif isinstance(message, BaseMessage): self.extend(message) @@ -448,4 +453,4 @@ class BaseMessage(list, abc.ABC): return f"{x} {y}" if y.type == "text" else x plain_text = reduce(_concat, self, "") - return plain_text[1:] if plain_text else plain_text + return plain_text.strip() diff --git a/nonebot/adapters/ding/__init__.py b/nonebot/adapters/ding/__init__.py new file mode 100644 index 00000000..8b5f101d --- /dev/null +++ b/nonebot/adapters/ding/__init__.py @@ -0,0 +1,15 @@ +""" +钉钉群机器人 协议适配 +============================ + +协议详情请看: `钉钉文档`_ + +.. _钉钉文档: + https://ding-doc.dingtalk.com/doc#/serverapi2/krgddi + +""" + +from .bot import Bot +from .event import Event +from .message import Message, MessageSegment +from .exception import ApiError, SessionExpired, AdapterException diff --git a/nonebot/adapters/ding/bot.py b/nonebot/adapters/ding/bot.py new file mode 100644 index 00000000..4acfc2fc --- /dev/null +++ b/nonebot/adapters/ding/bot.py @@ -0,0 +1,205 @@ +from datetime import datetime +import httpx + +from nonebot.log import logger +from nonebot.config import Config +from nonebot.message import handle_event +from nonebot.typing import Driver, WebSocket, NoReturn +from nonebot.typing import Any, Union, Optional +from nonebot.adapters import BaseBot +from nonebot.exception import NetworkError, RequestDenied, ApiNotAvailable +from .exception import ApiError, SessionExpired +from .utils import check_legal, log +from .event import Event +from .message import Message, MessageSegment +from .model import MessageModel + + +class Bot(BaseBot): + """ + 钉钉 协议 Bot 适配。继承属性参考 `BaseBot <./#class-basebot>`_ 。 + """ + + def __init__(self, + driver: Driver, + connection_type: str, + config: Config, + self_id: str, + *, + websocket: Optional[WebSocket] = None): + + super().__init__(driver, + connection_type, + config, + self_id, + websocket=websocket) + + @property + def type(self) -> str: + """ + - 返回: ``"ding"`` + """ + return "ding" + + @classmethod + async def check_permission(cls, driver: Driver, connection_type: str, + headers: dict, + body: Optional[dict]) -> Union[str, NoReturn]: + """ + :说明: + 钉钉协议鉴权。参考 `鉴权 `_ + """ + timestamp = headers.get("timestamp") + sign = headers.get("sign") + log("DEBUG", "headers: {}".format(headers)) + log("DEBUG", "body: {}".format(body)) + + # 检查 timestamp + if not timestamp: + log("WARNING", "Missing `timestamp` Header") + raise RequestDenied(400, "Missing `timestamp` Header") + # 检查 sign + if not sign: + log("WARNING", "Missing `sign` Header") + raise RequestDenied(400, "Missing `sign` Header") + # 校验 sign 和 timestamp,判断是否是来自钉钉的合法请求 + if not check_legal(timestamp, sign, driver): + log("WARNING", "Signature Header is invalid") + raise RequestDenied(403, "Signature is invalid") + # 检查连接方式 + if connection_type not in ["http"]: + log("WARNING", "Unsupported connection type") + raise RequestDenied(405, "Unsupported connection type") + + access_token = driver.config.access_token + if access_token and access_token != access_token: + log( + "WARNING", "Authorization Header is invalid" + if access_token else "Missing Authorization Header") + raise RequestDenied( + 403, "Authorization Header is invalid" + if access_token else "Missing Authorization Header") + return body.get("chatbotUserId") + + async def handle_message(self, body: dict): + message = MessageModel.parse_obj(body) + if not message: + return + log("DEBUG", "message: {}".format(message)) + + 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}" + ) + return + + async def call_api(self, api: str, **data) -> Union[Any, NoReturn]: + """ + :说明: + + 调用 钉钉 协议 API + + :参数: + + * ``api: str``: API 名称 + * ``**data: Any``: API 参数 + + :返回: + + - ``Any``: API 调用返回数据 + + :异常: + + - ``NetworkError``: 网络错误 + - ``ActionFailed``: API 调用失败 + """ + if "self_id" in data: + self_id = data.pop("self_id") + if self_id: + bot = self.driver.bots[str(self_id)] + return await bot.call_api(api, **data) + + log("DEBUG", f"Calling API {api}") + log("DEBUG", f"Calling data {data}") + + if self.connection_type == "http" and api == "post_webhook": + raw_event: MessageModel = data["raw_event"] + + if int(datetime.now().timestamp()) > int( + raw_event.sessionWebhookExpiredTime / 1000): + raise SessionExpired + + target = raw_event.sessionWebhook + + if not target: + raise ApiNotAvailable + + headers = {} + segment: MessageSegment = data["message"][0] + try: + async with httpx.AsyncClient(headers=headers) as client: + response = await client.post( + target, + params={"access_token": self.config.access_token}, + json=segment.data, + timeout=self.config.api_timeout) + + if 200 <= response.status_code < 300: + result = response.json() + if isinstance(result, dict): + if result.get("errcode") != 0: + raise ApiError(errcode=result.get("errcode"), + errmsg=result.get("errmsg")) + return result + raise NetworkError(f"HTTP request received unexpected " + f"status code: {response.status_code}") + except httpx.InvalidURL: + raise NetworkError("API root url invalid") + except httpx.HTTPError: + raise NetworkError("HTTP request failed") + + async def send(self, + event: "Event", + message: Union[str, "Message", "MessageSegment"], + at_sender: bool = False, + **kwargs) -> Union[Any, NoReturn]: + """ + :说明: + + 根据 ``event`` 向触发事件的主体发送消息。 + + :参数: + + * ``event: Event``: Event 对象 + * ``message: Union[str, Message, MessageSegment]``: 要发送的消息 + * ``at_sender: bool``: 是否 @ 事件主体 + * ``**kwargs``: 覆盖默认参数 + + :返回: + + - ``Any``: API 调用返回数据 + + :异常: + + - ``ValueError``: 缺少 ``user_id``, ``group_id`` + - ``NetworkError``: 网络错误 + - ``ActionFailed``: API 调用失败 + """ + msg = message if isinstance(message, Message) else Message(message) + log("DEBUG", f"send -> msg: {msg}") + + at_sender = at_sender and bool(event.user_id) + log("DEBUG", f"send -> at_sender: {at_sender}") + params = {"raw_event": event.raw_event} + params.update(kwargs) + + if at_sender and event.detail_type != "private": + params["message"] = f"@{event.user_id} " + msg + else: + params["message"] = msg + log("DEBUG", f"send -> params: {params}") + + return await self.call_api("post_webhook", **params) diff --git a/nonebot/adapters/ding/event.py b/nonebot/adapters/ding/event.py new file mode 100644 index 00000000..a4c50e9d --- /dev/null +++ b/nonebot/adapters/ding/event.py @@ -0,0 +1,207 @@ +from typing import Literal, Union +from nonebot.adapters import BaseEvent +from nonebot.typing import Optional + +from .utils import log +from .message import Message +from .model import MessageModel, ConversationType, TextMessage + + +class Event(BaseEvent): + """ + 钉钉 协议 Event 适配。继承属性参考 `BaseEvent <./#class-baseevent>`_ 。 + """ + + def __init__(self, message: MessageModel): + super().__init__(message) + if not message.msgtype: + log("ERROR", "message has no msgtype") + # 目前钉钉机器人只能接收到 text 类型的消息 + self._message = Message(getattr(message, message.msgtype or "text")) + + @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`` + - 说明: 事件名称,由类型与 ``.`` 组合而成 + """ + n = self.type + "." + self.detail_type + if self.sub_type: + n += "." + self.sub_type + return n + + @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: + """ + - 类型: ``str`` + - 说明: 事件类型 + """ + return "message" + + @type.setter + def type(self, value) -> None: + pass + + @property + def detail_type(self) -> Literal["private", "group"]: + """ + - 类型: ``str`` + - 说明: 事件详细类型 + """ + return self.raw_event.conversationType.name + + @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) -> Optional[str]: + """ + - 类型: ``Optional[str]`` + - 说明: 事件子类型 + """ + return "" + + @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 + + @to_me.setter + def to_me(self, value) -> None: + self.raw_event.isInAtList = value + + @property + def message(self) -> Optional["Message"]: + """ + - 类型: ``Optional[Message]`` + - 说明: 消息内容 + """ + return self._message + + @message.setter + def message(self, value) -> None: + self._message = value + + @property + def reply(self) -> None: + """ + - 类型: ``None`` + - 说明: 回复消息详情 + """ + raise ValueError("暂不支持 reply") + + @property + def raw_message(self) -> Optional[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]`` + - 说明: 纯文本消息内容 + """ + return self.message and self.message.extract_plain_text().strip() + + @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: + + 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") diff --git a/nonebot/adapters/ding/exception.py b/nonebot/adapters/ding/exception.py new file mode 100644 index 00000000..bfb318c5 --- /dev/null +++ b/nonebot/adapters/ding/exception.py @@ -0,0 +1,29 @@ +from nonebot.exception import AdapterException + + +class DingAdapterException(AdapterException): + + def __init__(self) -> None: + super.__init__("DING") + + +class ApiError(DingAdapterException): + """ + :说明: + + API 请求成功返回数据,但 API 操作失败。 + + """ + + def __init__(self, errcode: int, errmsg: str): + self.errcode = errcode + self.errmsg = errmsg + + def __repr__(self): + return f"" + + +class SessionExpired(DingAdapterException): + + def __repr__(self) -> str: + return f"" diff --git a/nonebot/adapters/ding/message.py b/nonebot/adapters/ding/message.py new file mode 100644 index 00000000..53b83f6e --- /dev/null +++ b/nonebot/adapters/ding/message.py @@ -0,0 +1,133 @@ +from nonebot.typing import Any, Dict, Union, Iterable +from nonebot.adapters import BaseMessage, 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}") + 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 "" + + 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 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 text(text: str) -> "MessageSegment": + return MessageSegment("text", {"text": {"content": text.strip()}}) + + @staticmethod + def markdown(title: str, text: str) -> "MessageSegment": + return MessageSegment("markdown", { + "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 + } + }) + + @staticmethod + def actionCardSingleMultiBtns( + title: str, + text: str, + btns: list = [], + hideAvatar: bool = False, + btnOrientation: str = '1', + ) -> "MessageSegment": + """ + :参数: + + * ``btnOrientation``: 0:按钮竖直排列 1:按钮横向排列 + + * ``btns``: [{ "title": title, "actionURL": actionURL }, ...] + """ + return MessageSegment( + "actionCard", { + "actionCard": { + "title": title, + "text": text, + "hideAvatar": "1" if hideAvatar else "0", + "btnOrientation": btnOrientation, + "btns": btns + } + }) + + @staticmethod + def feedCard(links: list = [],) -> "MessageSegment": + """ + :参数: + + * ``links``: [{ "title": xxx, "messageURL": xxx, "picURL": xxx }, ...] + """ + return MessageSegment("feedCard", {"feedCard": {"links": links}}) + + @staticmethod + def empty() -> "MessageSegment": + """不想回复消息到群里""" + return MessageSegment("empty") + + +class Message(BaseMessage): + """ + 钉钉 协议 Message 适配。 + """ + + @staticmethod + def _construct( + msg: Union[str, dict, list, + TextMessage]) -> 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(str) diff --git a/nonebot/adapters/ding/model.py b/nonebot/adapters/ding/model.py new file mode 100644 index 00000000..d317ea5b --- /dev/null +++ b/nonebot/adapters/ding/model.py @@ -0,0 +1,47 @@ +from typing import List, Optional +from enum import Enum +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): + msgtype: str = None + text: Optional[TextMessage] = None + msgId: str + # ms + createAt: int = None + conversationType: ConversationType = None + conversationId: str = None + conversationTitle: 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 + isInAtList: bool = None diff --git a/nonebot/adapters/ding/utils.py b/nonebot/adapters/ding/utils.py new file mode 100644 index 00000000..8c644683 --- /dev/null +++ b/nonebot/adapters/ding/utils.py @@ -0,0 +1,35 @@ +import base64 +import hashlib +import hmac +from typing import TYPE_CHECKING + +from nonebot.utils import logger_wrapper + +if TYPE_CHECKING: + from nonebot.drivers import BaseDriver +log = logger_wrapper("DING") + + +def check_legal(timestamp, remote_sign, driver: "BaseDriver"): + """ + 1. timestamp 与系统当前时间戳如果相差1小时以上,则认为是非法的请求。 + + 2. sign 与开发者自己计算的结果不一致,则认为是非法的请求。 + + 必须当timestamp和sign同时验证通过,才能认为是来自钉钉的合法请求。 + """ + # 目前先设置成 secret + # TODO 后面可能可以从 secret[adapter_name] 获取 + app_secret = driver.config.secret # 机器人的 appSecret + if not app_secret: + # TODO warning + log("WARNING", "No ding secrets set, won't check sign") + return True + app_secret_enc = app_secret.encode('utf-8') + string_to_sign = '{}\n{}'.format(timestamp, app_secret) + string_to_sign_enc = string_to_sign.encode('utf-8') + hmac_code = hmac.new(app_secret_enc, + string_to_sign_enc, + digestmod=hashlib.sha256).digest() + sign = base64.b64encode(hmac_code).decode('utf-8') + return remote_sign == sign diff --git a/nonebot/exception.py b/nonebot/exception.py index cc65e6da..1f61f5ed 100644 --- a/nonebot/exception.py +++ b/nonebot/exception.py @@ -145,3 +145,9 @@ class ActionFailed(Exception): def __str__(self): return self.__repr__() + + +class AdapterException(Exception): + + def __init__(self, adapter_name) -> None: + self.adapter_name = adapter_name diff --git a/nonebot/typing.py b/nonebot/typing.py index 09109b37..21a8b0ee 100644 --- a/nonebot/typing.py +++ b/nonebot/typing.py @@ -21,7 +21,7 @@ from types import ModuleType from typing import NoReturn, TYPE_CHECKING from typing import Any, Set, List, Dict, Type, Tuple, Mapping -from typing import Union, TypeVar, Optional, Iterable, Callable, Awaitable +from typing import Union, TypeVar, Optional, Iterable, Callable, Awaitable, Generic # import some modules needed when checking types if TYPE_CHECKING: diff --git a/tests/bot.py b/tests/bot.py index 45f99b95..16d3c5b0 100644 --- a/tests/bot.py +++ b/tests/bot.py @@ -5,6 +5,7 @@ sys.path.insert(0, os.path.abspath("..")) import nonebot from nonebot.adapters.cqhttp import Bot +from nonebot.adapters.ding import Bot as DingBot from nonebot.log import logger, default_format # test custom log @@ -18,6 +19,7 @@ nonebot.init(custom_config2="config on init") app = nonebot.get_asgi() driver = nonebot.get_driver() driver.register_adapter("cqhttp", Bot) +driver.register_adapter("ding", DingBot) # load builtin plugin nonebot.load_builtin_plugins() From 33bd9d0fb8186b51f8540dc357551833a7a85f92 Mon Sep 17 00:00:00 2001 From: nonebot Date: Wed, 2 Dec 2020 17:04:45 +0000 Subject: [PATCH 02/16] :memo: update api docs --- docs/api/adapters/README.md | 8 +- docs/api/adapters/cqhttp.md | 414 ------------------------------------ docs/api/exception.md | 6 + 3 files changed, 10 insertions(+), 418 deletions(-) diff --git a/docs/api/adapters/README.md b/docs/api/adapters/README.md index c2d2e399..b2e13b35 100644 --- a/docs/api/adapters/README.md +++ b/docs/api/adapters/README.md @@ -176,7 +176,7 @@ await bot.send_msg(message="hello world") ## _class_ `BaseEvent` -基类:`abc.ABC` +基类:`abc.ABC`, `typing.Generic` Event 基类。提供上报信息的关键信息,其余信息可从原始上报消息获取。 @@ -187,7 +187,7 @@ Event 基类。提供上报信息的关键信息,其余信息可从原始上 * **参数** - * `raw_event: dict`: 原始上报消息 + * `raw_event: T`: 原始上报消息 @@ -309,7 +309,7 @@ Event 基类。提供上报信息的关键信息,其余信息可从原始上 * **参数** - * `message: Union[str, dict, list, MessageSegment, Message]`: 消息内容 + * `message: Union[str, dict, list, BaseModel, MessageSegment, Message]`: 消息内容 @@ -350,7 +350,7 @@ Event 基类。提供上报信息的关键信息,其余信息可从原始上 * **说明** - 缩减消息数组,即拼接相邻纯文本消息段 + 缩减消息数组,即按 MessageSegment 的实现拼接相邻消息段 diff --git a/docs/api/adapters/cqhttp.md b/docs/api/adapters/cqhttp.md index ac9ecb11..47430815 100644 --- a/docs/api/adapters/cqhttp.md +++ b/docs/api/adapters/cqhttp.md @@ -8,417 +8,3 @@ sidebarDepth: 0 ## CQHTTP (OneBot) v11 协议适配 协议详情请看: [CQHTTP](https://github.com/howmanybots/onebot/blob/master/README.md) | [OneBot](https://github.com/howmanybots/onebot/blob/master/README.md) - - -## `log(level, message)` - - -* **说明** - - 用于打印 CQHTTP 日志。 - - - -* **参数** - - - * `level: str`: 日志等级 - - - * `message: str`: 日志信息 - - - -## `escape(s, *, escape_comma=True)` - - -* **说明** - - 对字符串进行 CQ 码转义。 - - - -* **参数** - - - * `s: str`: 需要转义的字符串 - - - * `escape_comma: bool`: 是否转义逗号(`,`)。 - - - -## `unescape(s)` - - -* **说明** - - 对字符串进行 CQ 码去转义。 - - - -* **参数** - - - * `s: str`: 需要转义的字符串 - - - -## `_b2s(b)` - -转换布尔值为字符串。 - - -## _async_ `_check_reply(bot, event)` - - -* **说明** - - 检查消息中存在的回复,去除并赋值 `event.reply`, `event.to_me` - - - -* **参数** - - - * `bot: Bot`: Bot 对象 - - - * `event: Event`: Event 对象 - - - -## `_check_at_me(bot, event)` - - -* **说明** - - 检查消息开头或结尾是否存在 @机器人,去除并赋值 `event.to_me` - - - -* **参数** - - - * `bot: Bot`: Bot 对象 - - - * `event: Event`: Event 对象 - - - -## `_check_nickname(bot, event)` - - -* **说明** - - 检查消息开头是否存在,去除并赋值 `event.to_me` - - - -* **参数** - - - * `bot: Bot`: Bot 对象 - - - * `event: Event`: Event 对象 - - - -## `_handle_api_result(result)` - - -* **说明** - - 处理 API 请求返回值。 - - - -* **参数** - - - * `result: Optional[Dict[str, Any]]`: API 返回数据 - - - -* **返回** - - - * `Any`: API 调用返回数据 - - - -* **异常** - - - * `ActionFailed`: API 调用失败 - - - -## _class_ `Bot` - -基类:[`nonebot.adapters.BaseBot`](README.md#nonebot.adapters.BaseBot) - -CQHTTP 协议 Bot 适配。继承属性参考 [BaseBot](./#class-basebot) 。 - - -### _property_ `type` - - -* 返回: `"cqhttp"` - - -### _async classmethod_ `check_permission(driver, connection_type, headers, body)` - - -* **说明** - - CQHTTP (OneBot) 协议鉴权。参考 [鉴权](https://github.com/howmanybots/onebot/blob/master/v11/specs/communication/authorization.md) - - - -### _async_ `handle_message(message)` - - -* **说明** - - 调用 [_check_reply](#async-check-reply-bot-event), [_check_at_me](#check-at-me-bot-event), [_check_nickname](#check-nickname-bot-event) 处理事件并转换为 [Event](#class-event) - - - -### _async_ `call_api(api, **data)` - - -* **说明** - - 调用 CQHTTP 协议 API - - - -* **参数** - - - * `api: str`: API 名称 - - - * `**data: Any`: API 参数 - - - -* **返回** - - - * `Any`: API 调用返回数据 - - - -* **异常** - - - * `NetworkError`: 网络错误 - - - * `ActionFailed`: API 调用失败 - - - -### _async_ `send(event, message, at_sender=False, **kwargs)` - - -* **说明** - - 根据 `event` 向触发事件的主体发送消息。 - - - -* **参数** - - - * `event: Event`: Event 对象 - - - * `message: Union[str, Message, MessageSegment]`: 要发送的消息 - - - * `at_sender: bool`: 是否 @ 事件主体 - - - * `**kwargs`: 覆盖默认参数 - - - -* **返回** - - - * `Any`: API 调用返回数据 - - - -* **异常** - - - * `ValueError`: 缺少 `user_id`, `group_id` - - - * `NetworkError`: 网络错误 - - - * `ActionFailed`: API 调用失败 - - - -## _class_ `Event` - -基类:[`nonebot.adapters.BaseEvent`](README.md#nonebot.adapters.BaseEvent) - -CQHTTP 协议 Event 适配。继承属性参考 [BaseEvent](./#class-baseevent) 。 - - -### _property_ `id` - - -* 类型: `Optional[int]` - - -* 说明: 事件/消息 ID - - -### _property_ `name` - - -* 类型: `str` - - -* 说明: 事件名称,由类型与 `.` 组合而成 - - -### _property_ `self_id` - - -* 类型: `str` - - -* 说明: 机器人自身 ID - - -### _property_ `time` - - -* 类型: `int` - - -* 说明: 事件发生时间 - - -### _property_ `type` - - -* 类型: `str` - - -* 说明: 事件类型 - - -### _property_ `detail_type` - - -* 类型: `str` - - -* 说明: 事件详细类型 - - -### _property_ `sub_type` - - -* 类型: `Optional[str]` - - -* 说明: 事件子类型 - - -### _property_ `user_id` - - -* 类型: `Optional[int]` - - -* 说明: 事件主体 ID - - -### _property_ `group_id` - - -* 类型: `Optional[int]` - - -* 说明: 事件主体群 ID - - -### _property_ `to_me` - - -* 类型: `Optional[bool]` - - -* 说明: 消息是否与机器人相关 - - -### _property_ `message` - - -* 类型: `Optional[Message]` - - -* 说明: 消息内容 - - -### _property_ `reply` - - -* 类型: `Optional[dict]` - - -* 说明: 回复消息详情 - - -### _property_ `raw_message` - - -* 类型: `Optional[str]` - - -* 说明: 原始消息 - - -### _property_ `plain_text` - - -* 类型: `Optional[str]` - - -* 说明: 纯文本消息内容 - - -### _property_ `sender` - - -* 类型: `Optional[dict]` - - -* 说明: 消息发送者信息 - - -## _class_ `MessageSegment` - -基类:[`nonebot.adapters.BaseMessageSegment`](README.md#nonebot.adapters.BaseMessageSegment) - -CQHTTP 协议 MessageSegment 适配。具体方法参考协议消息段类型或源码。 - - -## _class_ `Message` - -基类:[`nonebot.adapters.BaseMessage`](README.md#nonebot.adapters.BaseMessage) - -CQHTTP 协议 Message 适配。 diff --git a/docs/api/exception.md b/docs/api/exception.md index 0a9876a8..a4f58a82 100644 --- a/docs/api/exception.md +++ b/docs/api/exception.md @@ -158,3 +158,9 @@ sidebarDepth: 0 * `retcode: Optional[int]`: 错误代码 + + + +## _exception_ `AdapterException` + +基类:`Exception` From 8c5c02f040cdaf80b29f4e22d2213ce9cbdd0ee9 Mon Sep 17 00:00:00 2001 From: Artin Date: Thu, 3 Dec 2020 01:46:51 +0800 Subject: [PATCH 03/16] :label: Update `BaseEvent` typing --- nonebot/adapters/__init__.py | 10 +++++----- nonebot/adapters/ding/event.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index a7dd7b21..271ff543 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -137,27 +137,27 @@ class BaseBot(abc.ABC): raise NotImplementedError -T = TypeVar("T", dict, BaseModel) +T = TypeVar("T", bound=BaseModel) -class BaseEvent(abc.ABC, Generic[T]): +class BaseEvent(Generic[T], abc.ABC): """ Event 基类。提供上报信息的关键信息,其余信息可从原始上报消息获取。 """ - def __init__(self, raw_event: T): + def __init__(self, raw_event: Union[dict, T]): """ :参数: * ``raw_event: T``: 原始上报消息 """ - self._raw_event: T = raw_event + self._raw_event = raw_event def __repr__(self) -> str: return f"" @property - def raw_event(self) -> T: + def raw_event(self) -> Union[dict, T]: """原始上报消息""" return self._raw_event diff --git a/nonebot/adapters/ding/event.py b/nonebot/adapters/ding/event.py index a4c50e9d..f462f0d3 100644 --- a/nonebot/adapters/ding/event.py +++ b/nonebot/adapters/ding/event.py @@ -1,4 +1,5 @@ -from typing import Literal, Union +from typing import Literal + from nonebot.adapters import BaseEvent from nonebot.typing import Optional From f1a0ac099bd3da23666b85a2054e4eeb8f2b9fd4 Mon Sep 17 00:00:00 2001 From: nonebot Date: Wed, 2 Dec 2020 17:49:14 +0000 Subject: [PATCH 04/16] :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 b2e13b35..ce0bc1d9 100644 --- a/docs/api/adapters/README.md +++ b/docs/api/adapters/README.md @@ -176,7 +176,7 @@ await bot.send_msg(message="hello world") ## _class_ `BaseEvent` -基类:`abc.ABC`, `typing.Generic` +基类:`typing.Generic`, `abc.ABC` Event 基类。提供上报信息的关键信息,其余信息可从原始上报消息获取。 From e718f25c3fc0bf3da66bd70699b80da47a1a98af Mon Sep 17 00:00:00 2001 From: Artin Date: Thu, 3 Dec 2020 12:08:04 +0800 Subject: [PATCH 05/16] =?UTF-8?q?:label:=20=E5=AE=8C=E5=96=84=20typing=20?= =?UTF-8?q?=E5=8F=8A=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_build/adapters/ding.rst | 12 +++++++++++ nonebot/adapters/__init__.py | 4 ++-- nonebot/adapters/ding/__init__.py | 2 +- nonebot/adapters/ding/bot.py | 33 +++++++++--------------------- nonebot/adapters/ding/event.py | 27 +++++++++--------------- nonebot/adapters/ding/exception.py | 14 ++++++++++++- nonebot/exception.py | 11 +++++++++- 7 files changed, 58 insertions(+), 45 deletions(-) create mode 100644 docs_build/adapters/ding.rst diff --git a/docs_build/adapters/ding.rst b/docs_build/adapters/ding.rst new file mode 100644 index 00000000..b26dac19 --- /dev/null +++ b/docs_build/adapters/ding.rst @@ -0,0 +1,12 @@ +--- +contentSidebar: true +sidebarDepth: 0 +--- + +NoneBot.adapters.ding 模块 +============================ + +.. automodule:: nonebot.adapters.ding + :members: + :private-members: + :show-inheritance: diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index 271ff543..4d1fa60c 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -140,7 +140,7 @@ class BaseBot(abc.ABC): T = TypeVar("T", bound=BaseModel) -class BaseEvent(Generic[T], abc.ABC): +class BaseEvent(abc.ABC, Generic[T]): """ Event 基类。提供上报信息的关键信息,其余信息可从原始上报消息获取。 """ @@ -149,7 +149,7 @@ class BaseEvent(Generic[T], abc.ABC): """ :参数: - * ``raw_event: T``: 原始上报消息 + * ``raw_event: Union[dict, T]``: 原始上报消息 """ self._raw_event = raw_event diff --git a/nonebot/adapters/ding/__init__.py b/nonebot/adapters/ding/__init__.py index 8b5f101d..e9742bf4 100644 --- a/nonebot/adapters/ding/__init__.py +++ b/nonebot/adapters/ding/__init__.py @@ -12,4 +12,4 @@ from .bot import Bot from .event import Event from .message import Message, MessageSegment -from .exception import ApiError, SessionExpired, AdapterException +from .exception import ApiError, SessionExpired, DingAdapterException diff --git a/nonebot/adapters/ding/bot.py b/nonebot/adapters/ding/bot.py index 4acfc2fc..bfc106d0 100644 --- a/nonebot/adapters/ding/bot.py +++ b/nonebot/adapters/ding/bot.py @@ -4,7 +4,7 @@ import httpx from nonebot.log import logger from nonebot.config import Config from nonebot.message import handle_event -from nonebot.typing import Driver, WebSocket, NoReturn +from nonebot.typing import Driver, NoReturn from nonebot.typing import Any, Union, Optional from nonebot.adapters import BaseBot from nonebot.exception import NetworkError, RequestDenied, ApiNotAvailable @@ -20,19 +20,10 @@ class Bot(BaseBot): 钉钉 协议 Bot 适配。继承属性参考 `BaseBot <./#class-basebot>`_ 。 """ - def __init__(self, - driver: Driver, - connection_type: str, - config: Config, - self_id: str, - *, - websocket: Optional[WebSocket] = None): + def __init__(self, driver: Driver, connection_type: str, config: Config, + self_id: str, **kwargs): - super().__init__(driver, - connection_type, - config, - self_id, - websocket=websocket) + super().__init__(driver, connection_type, config, self_id, **kwargs) @property def type(self) -> str: @@ -56,26 +47,19 @@ class Bot(BaseBot): # 检查 timestamp if not timestamp: - log("WARNING", "Missing `timestamp` Header") raise RequestDenied(400, "Missing `timestamp` Header") # 检查 sign if not sign: - log("WARNING", "Missing `sign` Header") raise RequestDenied(400, "Missing `sign` Header") # 校验 sign 和 timestamp,判断是否是来自钉钉的合法请求 if not check_legal(timestamp, sign, driver): - log("WARNING", "Signature Header is invalid") raise RequestDenied(403, "Signature is invalid") # 检查连接方式 if connection_type not in ["http"]: - log("WARNING", "Unsupported connection type") raise RequestDenied(405, "Unsupported connection type") access_token = driver.config.access_token if access_token and access_token != access_token: - log( - "WARNING", "Authorization Header is invalid" - if access_token else "Missing Authorization Header") raise RequestDenied( 403, "Authorization Header is invalid" if access_token else "Missing Authorization Header") @@ -116,6 +100,9 @@ class Bot(BaseBot): - ``NetworkError``: 网络错误 - ``ActionFailed``: API 调用失败 """ + if self.connection_type != "http": + log("ERROR", "Only support http connection.") + return if "self_id" in data: self_id = data.pop("self_id") if self_id: @@ -125,9 +112,9 @@ class Bot(BaseBot): log("DEBUG", f"Calling API {api}") log("DEBUG", f"Calling data {data}") - if self.connection_type == "http" and api == "post_webhook": + if api == "send_message": raw_event: MessageModel = data["raw_event"] - + # 确保 sessionWebhook 没有过期 if int(datetime.now().timestamp()) > int( raw_event.sessionWebhookExpiredTime / 1000): raise SessionExpired @@ -202,4 +189,4 @@ class Bot(BaseBot): params["message"] = msg log("DEBUG", f"send -> params: {params}") - return await self.call_api("post_webhook", **params) + return await self.call_api("send_message", **params) diff --git a/nonebot/adapters/ding/event.py b/nonebot/adapters/ding/event.py index f462f0d3..9c9fb50f 100644 --- a/nonebot/adapters/ding/event.py +++ b/nonebot/adapters/ding/event.py @@ -1,9 +1,7 @@ -from typing import Literal +from typing import Literal, Union, Optional from nonebot.adapters import BaseEvent -from nonebot.typing import Optional -from .utils import log from .message import Message from .model import MessageModel, ConversationType, TextMessage @@ -15,9 +13,7 @@ class Event(BaseEvent): def __init__(self, message: MessageModel): super().__init__(message) - if not message.msgtype: - log("ERROR", "message has no msgtype") - # 目前钉钉机器人只能接收到 text 类型的消息 + # 其实目前钉钉机器人只能接收到 text 类型的消息 self._message = Message(getattr(message, message.msgtype or "text")) @property @@ -37,12 +33,9 @@ class Event(BaseEvent): def name(self) -> str: """ - 类型: ``str`` - - 说明: 事件名称,由类型与 ``.`` 组合而成 + - 说明: 事件名称,由 `type`.`detail_type` 组合而成 """ - n = self.type + "." + self.detail_type - if self.sub_type: - n += "." + self.sub_type - return n + return self.type + "." + self.detail_type @property def self_id(self) -> str: @@ -89,12 +82,12 @@ class Event(BaseEvent): self.raw_event.conversationType = ConversationType.group @property - def sub_type(self) -> Optional[str]: + def sub_type(self) -> None: """ - - 类型: ``Optional[str]`` - - 说明: 事件子类型 + - 类型: ``None`` + - 说明: 钉钉适配器无事件子类型 """ - return "" + return None @sub_type.setter def sub_type(self, value) -> None: @@ -134,7 +127,7 @@ class Event(BaseEvent): @to_me.setter def to_me(self, value) -> None: - self.raw_event.isInAtList = value + pass @property def message(self) -> Optional["Message"]: @@ -157,7 +150,7 @@ class Event(BaseEvent): raise ValueError("暂不支持 reply") @property - def raw_message(self) -> Optional[TextMessage]: + def raw_message(self) -> Optional[Union[TextMessage]]: """ - 类型: ``Optional[str]`` - 说明: 原始消息 diff --git a/nonebot/adapters/ding/exception.py b/nonebot/adapters/ding/exception.py index bfb318c5..7b845afe 100644 --- a/nonebot/adapters/ding/exception.py +++ b/nonebot/adapters/ding/exception.py @@ -2,6 +2,12 @@ from nonebot.exception import AdapterException class DingAdapterException(AdapterException): + """ + :说明: + + 钉钉 Adapter 错误基类 + + """ def __init__(self) -> None: super.__init__("DING") @@ -11,7 +17,7 @@ class ApiError(DingAdapterException): """ :说明: - API 请求成功返回数据,但 API 操作失败。 + API 请求返回错误信息。 """ @@ -24,6 +30,12 @@ class ApiError(DingAdapterException): class SessionExpired(DingAdapterException): + """ + :说明: + + 发消息的 session 已经过期。 + + """ def __repr__(self) -> str: return f"" diff --git a/nonebot/exception.py b/nonebot/exception.py index 1f61f5ed..b374f4c5 100644 --- a/nonebot/exception.py +++ b/nonebot/exception.py @@ -148,6 +148,15 @@ class ActionFailed(Exception): class AdapterException(Exception): + """ + :说明: - def __init__(self, adapter_name) -> None: + 代表 Adapter 抛出的异常,所有的 Adapter 都要在内部继承自这个 `Exception` + + :参数: + + * ``adapter_name: str``: 标识 adapter + """ + + def __init__(self, adapter_name: str) -> None: self.adapter_name = adapter_name From da62e012c22a483cbda58a764e813276c9cb167a Mon Sep 17 00:00:00 2001 From: nonebot Date: Thu, 3 Dec 2020 04:09:54 +0000 Subject: [PATCH 06/16] :memo: update api docs --- docs/api/adapters/README.md | 4 ++-- docs/api/adapters/ding.md | 10 ++++++++++ docs/api/exception.md | 12 ++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 docs/api/adapters/ding.md diff --git a/docs/api/adapters/README.md b/docs/api/adapters/README.md index ce0bc1d9..1a1dd85b 100644 --- a/docs/api/adapters/README.md +++ b/docs/api/adapters/README.md @@ -176,7 +176,7 @@ await bot.send_msg(message="hello world") ## _class_ `BaseEvent` -基类:`typing.Generic`, `abc.ABC` +基类:`abc.ABC`, `typing.Generic` Event 基类。提供上报信息的关键信息,其余信息可从原始上报消息获取。 @@ -187,7 +187,7 @@ Event 基类。提供上报信息的关键信息,其余信息可从原始上 * **参数** - * `raw_event: T`: 原始上报消息 + * `raw_event: Union[dict, T]`: 原始上报消息 diff --git a/docs/api/adapters/ding.md b/docs/api/adapters/ding.md new file mode 100644 index 00000000..10d0aa42 --- /dev/null +++ b/docs/api/adapters/ding.md @@ -0,0 +1,10 @@ +--- +contentSidebar: true +sidebarDepth: 0 +--- + +# NoneBot.adapters.ding 模块 + +## 钉钉群机器人 协议适配 + +协议详情请看: [钉钉文档](https://ding-doc.dingtalk.com/doc#/serverapi2/krgddi) diff --git a/docs/api/exception.md b/docs/api/exception.md index a4f58a82..42284e7e 100644 --- a/docs/api/exception.md +++ b/docs/api/exception.md @@ -164,3 +164,15 @@ sidebarDepth: 0 ## _exception_ `AdapterException` 基类:`Exception` + + +* **说明** + + 代表 Adapter 抛出的异常,所有的 Adapter 都要在内部继承自这个 Exception + + + +* **参数** + + + * `adapter_name: str`: 标识 adapter From 3638da7f2d9c97ce23c43fcd124a51026379f05b Mon Sep 17 00:00:00 2001 From: Artin Date: Thu, 3 Dec 2020 12:22:39 +0800 Subject: [PATCH 07/16] :bug: Fix ding adapter exception --- nonebot/adapters/ding/exception.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nonebot/adapters/ding/exception.py b/nonebot/adapters/ding/exception.py index 7b845afe..37630028 100644 --- a/nonebot/adapters/ding/exception.py +++ b/nonebot/adapters/ding/exception.py @@ -10,7 +10,7 @@ class DingAdapterException(AdapterException): """ def __init__(self) -> None: - super.__init__("DING") + super().__init__("DING") class ApiError(DingAdapterException): @@ -22,6 +22,7 @@ class ApiError(DingAdapterException): """ def __init__(self, errcode: int, errmsg: str): + super().__init__() self.errcode = errcode self.errmsg = errmsg From 7116532081a61db3e0b2fddf7439767d0a036c99 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Thu, 3 Dec 2020 13:22:12 +0800 Subject: [PATCH 08/16] :construction: add exception --- nonebot/exception.py | 59 +++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/nonebot/exception.py b/nonebot/exception.py index b374f4c5..4df3ea49 100644 --- a/nonebot/exception.py +++ b/nonebot/exception.py @@ -6,10 +6,19 @@ 这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。 """ -from nonebot.typing import List, Type, Optional +from nonebot.typing import Optional -class IgnoredException(Exception): +class NoneBotException(Exception): + """ + :说明: + + 所有 NoneBot 发生的异常基类。 + """ + pass + + +class IgnoredException(NoneBotException): """ :说明: @@ -30,7 +39,7 @@ class IgnoredException(Exception): return self.__repr__() -class PausedException(Exception): +class PausedException(NoneBotException): """ :说明: @@ -44,7 +53,7 @@ class PausedException(Exception): pass -class RejectedException(Exception): +class RejectedException(NoneBotException): """ :说明: @@ -58,7 +67,7 @@ class RejectedException(Exception): pass -class FinishedException(Exception): +class FinishedException(NoneBotException): """ :说明: @@ -72,7 +81,7 @@ class FinishedException(Exception): pass -class StopPropagation(Exception): +class StopPropagation(NoneBotException): """ :说明: @@ -85,7 +94,7 @@ class StopPropagation(Exception): pass -class RequestDenied(Exception): +class RequestDenied(NoneBotException): """ :说明: @@ -108,7 +117,22 @@ class RequestDenied(Exception): return self.__repr__() -class ApiNotAvailable(Exception): +class AdapterException(NoneBotException): + """ + :说明: + + 代表 ``Adapter`` 抛出的异常,所有的 ``Adapter`` 都要在内部继承自这个 ``Exception`` + + :参数: + + * ``adapter_name: str``: 标识 adapter + """ + + def __init__(self, adapter_name: str) -> None: + self.adapter_name = adapter_name + + +class ApiNotAvailable(AdapterException): """ :说明: @@ -117,7 +141,7 @@ class ApiNotAvailable(Exception): pass -class NetworkError(Exception): +class NetworkError(AdapterException): """ :说明: @@ -126,7 +150,7 @@ class NetworkError(Exception): pass -class ActionFailed(Exception): +class ActionFailed(AdapterException): """ :说明: @@ -145,18 +169,3 @@ class ActionFailed(Exception): def __str__(self): return self.__repr__() - - -class AdapterException(Exception): - """ - :说明: - - 代表 Adapter 抛出的异常,所有的 Adapter 都要在内部继承自这个 `Exception` - - :参数: - - * ``adapter_name: str``: 标识 adapter - """ - - def __init__(self, adapter_name: str) -> None: - self.adapter_name = adapter_name From 8f89e0f26eebee149914e8ca20fe66669956544b Mon Sep 17 00:00:00 2001 From: nonebot Date: Thu, 3 Dec 2020 05:23:47 +0000 Subject: [PATCH 09/16] :memo: update api docs --- docs/api/exception.md | 65 +++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/docs/api/exception.md b/docs/api/exception.md index 42284e7e..26bd95a6 100644 --- a/docs/api/exception.md +++ b/docs/api/exception.md @@ -11,11 +11,22 @@ sidebarDepth: 0 这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。 -## _exception_ `IgnoredException` +## _exception_ `NoneBotException` 基类:`Exception` +* **说明** + + 所有 NoneBot 发生的异常基类。 + + + +## _exception_ `IgnoredException` + +基类:`nonebot.exception.NoneBotException` + + * **说明** 指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。 @@ -31,7 +42,7 @@ sidebarDepth: 0 ## _exception_ `PausedException` -基类:`Exception` +基类:`nonebot.exception.NoneBotException` * **说明** @@ -49,7 +60,7 @@ sidebarDepth: 0 ## _exception_ `RejectedException` -基类:`Exception` +基类:`nonebot.exception.NoneBotException` * **说明** @@ -67,7 +78,7 @@ sidebarDepth: 0 ## _exception_ `FinishedException` -基类:`Exception` +基类:`nonebot.exception.NoneBotException` * **说明** @@ -85,7 +96,7 @@ sidebarDepth: 0 ## _exception_ `StopPropagation` -基类:`Exception` +基类:`nonebot.exception.NoneBotException` * **说明** @@ -102,7 +113,7 @@ sidebarDepth: 0 ## _exception_ `RequestDenied` -基类:`Exception` +基类:`nonebot.exception.NoneBotException` * **说明** @@ -121,9 +132,27 @@ sidebarDepth: 0 +## _exception_ `AdapterException` + +基类:`nonebot.exception.NoneBotException` + + +* **说明** + + 代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception` + + + +* **参数** + + + * `adapter_name: str`: 标识 adapter + + + ## _exception_ `ApiNotAvailable` -基类:`Exception` +基类:`nonebot.exception.AdapterException` * **说明** @@ -134,7 +163,7 @@ sidebarDepth: 0 ## _exception_ `NetworkError` -基类:`Exception` +基类:`nonebot.exception.AdapterException` * **说明** @@ -145,7 +174,7 @@ sidebarDepth: 0 ## _exception_ `ActionFailed` -基类:`Exception` +基类:`nonebot.exception.AdapterException` * **说明** @@ -158,21 +187,3 @@ sidebarDepth: 0 * `retcode: Optional[int]`: 错误代码 - - - -## _exception_ `AdapterException` - -基类:`Exception` - - -* **说明** - - 代表 Adapter 抛出的异常,所有的 Adapter 都要在内部继承自这个 Exception - - - -* **参数** - - - * `adapter_name: str`: 标识 adapter From 9658e446e57f5ec7210435ef684d0c2b1185457e Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Thu, 3 Dec 2020 15:07:03 +0800 Subject: [PATCH 10/16] :building_construction: change exception structure --- nonebot/adapters/cqhttp/__init__.py | 5 +++-- nonebot/adapters/cqhttp/bot.py | 25 ++++++++++++------------ nonebot/adapters/cqhttp/exception.py | 29 ++++++++++++++++++++++++++++ nonebot/adapters/ding/bot.py | 21 ++++++++------------ nonebot/adapters/ding/exception.py | 10 +++++----- nonebot/exception.py | 14 +------------- 6 files changed, 59 insertions(+), 45 deletions(-) create mode 100644 nonebot/adapters/cqhttp/exception.py diff --git a/nonebot/adapters/cqhttp/__init__.py b/nonebot/adapters/cqhttp/__init__.py index 93c993b0..5de85a92 100644 --- a/nonebot/adapters/cqhttp/__init__.py +++ b/nonebot/adapters/cqhttp/__init__.py @@ -10,6 +10,7 @@ CQHTTP (OneBot) v11 协议适配 https://github.com/howmanybots/onebot/blob/master/README.md """ -from .message import Message, MessageSegment -from .bot import Bot +from .utils import log from .event import Event +from .message import Message, MessageSegment +from .bot import Bot, _check_at_me, _check_nickname, _check_reply diff --git a/nonebot/adapters/cqhttp/bot.py b/nonebot/adapters/cqhttp/bot.py index f4df0c6c..5c7ac7a4 100644 --- a/nonebot/adapters/cqhttp/bot.py +++ b/nonebot/adapters/cqhttp/bot.py @@ -8,15 +8,16 @@ import httpx from nonebot.log import logger from nonebot.config import Config -from nonebot.message import handle_event -from nonebot.typing import overrides, Driver, WebSocket, NoReturn -from nonebot.typing import Any, Dict, Union, Optional from nonebot.adapters import BaseBot -from nonebot.exception import NetworkError, ActionFailed, RequestDenied, ApiNotAvailable +from nonebot.message import handle_event +from nonebot.typing import Any, Dict, Union, Optional +from nonebot.typing import overrides, Driver, WebSocket, NoReturn +from nonebot.exception import NetworkError, RequestDenied, ApiNotAvailable -from .message import Message, MessageSegment -from .utils import log, get_auth_bearer from .event import Event +from .exception import ApiError +from .utils import log, get_auth_bearer +from .message import Message, MessageSegment async def _check_reply(bot: "Bot", event: "Event"): @@ -159,11 +160,11 @@ def _handle_api_result( :异常: - - ``ActionFailed``: API 调用失败 + - ``ApiError``: API 调用失败 """ if isinstance(result, dict): if result.get("status") == "failed": - raise ActionFailed(retcode=result.get("retcode")) + raise ApiError(retcode=result.get("retcode")) return result.get("data") @@ -317,7 +318,7 @@ class Bot(BaseBot): :异常: - ``NetworkError``: 网络错误 - - ``ActionFailed``: API 调用失败 + - ``ApiError``: API 调用失败 """ if "self_id" in data: self_id = data.pop("self_id") @@ -368,8 +369,8 @@ class Bot(BaseBot): @overrides(BaseBot) async def send(self, - event: "Event", - message: Union[str, "Message", "MessageSegment"], + event: Event, + message: Union[str, Message, MessageSegment], at_sender: bool = False, **kwargs) -> Union[Any, NoReturn]: """ @@ -392,7 +393,7 @@ class Bot(BaseBot): - ``ValueError``: 缺少 ``user_id``, ``group_id`` - ``NetworkError``: 网络错误 - - ``ActionFailed``: API 调用失败 + - ``ApiError``: API 调用失败 """ msg = message if isinstance(message, Message) else Message(message) diff --git a/nonebot/adapters/cqhttp/exception.py b/nonebot/adapters/cqhttp/exception.py new file mode 100644 index 00000000..86fe963c --- /dev/null +++ b/nonebot/adapters/cqhttp/exception.py @@ -0,0 +1,29 @@ +from nonebot.exception import AdapterException, ActionFailed + + +class CQHTTPAdapterException(AdapterException): + + def __init__(self): + super().__init__("cqhttp") + + +class ApiError(CQHTTPAdapterException, ActionFailed): + """ + :说明: + + API 请求返回错误信息。 + + :参数: + + * ``retcode: Optional[int]``: 错误码 + """ + + def __init__(self, retcode: Optional[int] = None): + super().__init__() + self.retcode = retcode + + def __repr__(self): + return f"" + + def __str__(self): + return self.__repr__() diff --git a/nonebot/adapters/ding/bot.py b/nonebot/adapters/ding/bot.py index bfc106d0..97ff7c2e 100644 --- a/nonebot/adapters/ding/bot.py +++ b/nonebot/adapters/ding/bot.py @@ -1,18 +1,19 @@ -from datetime import datetime import httpx +from datetime import datetime from nonebot.log import logger from nonebot.config import Config +from nonebot.adapters import BaseBot from nonebot.message import handle_event from nonebot.typing import Driver, NoReturn from nonebot.typing import Any, Union, Optional -from nonebot.adapters import BaseBot from nonebot.exception import NetworkError, RequestDenied, ApiNotAvailable -from .exception import ApiError, SessionExpired -from .utils import check_legal, log + from .event import Event -from .message import Message, MessageSegment from .model import MessageModel +from .utils import check_legal, log +from .message import Message, MessageSegment +from .exception import ApiError, SessionExpired class Bot(BaseBot): @@ -38,12 +39,11 @@ class Bot(BaseBot): body: Optional[dict]) -> Union[str, NoReturn]: """ :说明: + 钉钉协议鉴权。参考 `鉴权 `_ """ timestamp = headers.get("timestamp") sign = headers.get("sign") - log("DEBUG", "headers: {}".format(headers)) - log("DEBUG", "body: {}".format(body)) # 检查 timestamp if not timestamp: @@ -69,7 +69,6 @@ class Bot(BaseBot): message = MessageModel.parse_obj(body) if not message: return - log("DEBUG", "message: {}".format(message)) try: event = Event(message) @@ -110,7 +109,6 @@ class Bot(BaseBot): return await bot.call_api(api, **data) log("DEBUG", f"Calling API {api}") - log("DEBUG", f"Calling data {data}") if api == "send_message": raw_event: MessageModel = data["raw_event"] @@ -149,7 +147,7 @@ class Bot(BaseBot): raise NetworkError("HTTP request failed") async def send(self, - event: "Event", + event: Event, message: Union[str, "Message", "MessageSegment"], at_sender: bool = False, **kwargs) -> Union[Any, NoReturn]: @@ -176,10 +174,8 @@ class Bot(BaseBot): - ``ActionFailed``: API 调用失败 """ msg = message if isinstance(message, Message) else Message(message) - log("DEBUG", f"send -> msg: {msg}") at_sender = at_sender and bool(event.user_id) - log("DEBUG", f"send -> at_sender: {at_sender}") params = {"raw_event": event.raw_event} params.update(kwargs) @@ -187,6 +183,5 @@ class Bot(BaseBot): params["message"] = f"@{event.user_id} " + msg else: params["message"] = msg - log("DEBUG", f"send -> params: {params}") return await self.call_api("send_message", **params) diff --git a/nonebot/adapters/ding/exception.py b/nonebot/adapters/ding/exception.py index 37630028..ad6f4a20 100644 --- a/nonebot/adapters/ding/exception.py +++ b/nonebot/adapters/ding/exception.py @@ -1,4 +1,4 @@ -from nonebot.exception import AdapterException +from nonebot.exception import AdapterException, ActionFailed, ApiNotAvailable class DingAdapterException(AdapterException): @@ -10,10 +10,10 @@ class DingAdapterException(AdapterException): """ def __init__(self) -> None: - super().__init__("DING") + super().__init__("ding") -class ApiError(DingAdapterException): +class ApiError(DingAdapterException, ActionFailed): """ :说明: @@ -30,7 +30,7 @@ class ApiError(DingAdapterException): return f"" -class SessionExpired(DingAdapterException): +class SessionExpired(DingAdapterException, ApiNotAvailable): """ :说明: @@ -39,4 +39,4 @@ class SessionExpired(DingAdapterException): """ def __repr__(self) -> str: - return f"" + return f"" diff --git a/nonebot/exception.py b/nonebot/exception.py index 4df3ea49..b97c3f08 100644 --- a/nonebot/exception.py +++ b/nonebot/exception.py @@ -155,17 +155,5 @@ class ActionFailed(AdapterException): :说明: API 请求成功返回数据,但 API 操作失败。 - - :参数: - - * ``retcode: Optional[int]``: 错误代码 """ - - def __init__(self, retcode: Optional[int]): - self.retcode = retcode - - def __repr__(self): - return f"" - - def __str__(self): - return self.__repr__() + pass From 4b764ccba3860a878021401ff76802e90b846e30 Mon Sep 17 00:00:00 2001 From: nonebot Date: Thu, 3 Dec 2020 07:09:09 +0000 Subject: [PATCH 11/16] :memo: update api docs --- docs/api/adapters/cqhttp.md | 4 ---- docs/api/exception.md | 7 ------- 2 files changed, 11 deletions(-) diff --git a/docs/api/adapters/cqhttp.md b/docs/api/adapters/cqhttp.md index 47430815..6dd7f4a9 100644 --- a/docs/api/adapters/cqhttp.md +++ b/docs/api/adapters/cqhttp.md @@ -4,7 +4,3 @@ sidebarDepth: 0 --- # NoneBot.adapters.cqhttp 模块 - -## CQHTTP (OneBot) v11 协议适配 - -协议详情请看: [CQHTTP](https://github.com/howmanybots/onebot/blob/master/README.md) | [OneBot](https://github.com/howmanybots/onebot/blob/master/README.md) diff --git a/docs/api/exception.md b/docs/api/exception.md index 26bd95a6..0c584f75 100644 --- a/docs/api/exception.md +++ b/docs/api/exception.md @@ -180,10 +180,3 @@ sidebarDepth: 0 * **说明** API 请求成功返回数据,但 API 操作失败。 - - - -* **参数** - - - * `retcode: Optional[int]`: 错误代码 From dc691889e334dec9ba9bfac09ddbe3dd70dcb88b Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Thu, 3 Dec 2020 16:04:14 +0800 Subject: [PATCH 12/16] :art: improve cqhttp --- docs/.vuepress/config.js | 4 + docs/api/README.md | 3 + docs/api/adapters/cqhttp.md | 436 ++++++++++++++++++ docs_build/README.rst | 1 + docs_build/adapters/cqhttp.rst | 23 +- nonebot/adapters/cqhttp/__init__.py | 4 +- nonebot/adapters/cqhttp/bot.py | 24 +- .../adapters/cqhttp/{__init__.pyi => bot.pyi} | 270 +---------- nonebot/adapters/cqhttp/event.py | 3 +- nonebot/adapters/cqhttp/exception.py | 34 +- nonebot/adapters/cqhttp/message.py | 1 + nonebot/adapters/cqhttp/utils.py | 14 +- nonebot/exception.py | 2 - 13 files changed, 529 insertions(+), 290 deletions(-) rename nonebot/adapters/cqhttp/{__init__.pyi => bot.pyi} (78%) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index d9d942ab..5d7cf2fb 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -198,6 +198,10 @@ module.exports = context => ({ { title: "nonebot.adapters.cqhttp 模块", path: "adapters/cqhttp" + }, + { + title: "nonebot.adapters.ding 模块", + path: "adapters/ding" } ] } diff --git a/docs/api/README.md b/docs/api/README.md index 810b1964..243733f8 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -47,3 +47,6 @@ * [nonebot.adapters.cqhttp](adapters/cqhttp.html) + + + * [nonebot.adapters.ding](adapters/ding.html) diff --git a/docs/api/adapters/cqhttp.md b/docs/api/adapters/cqhttp.md index 6dd7f4a9..b80d6ad1 100644 --- a/docs/api/adapters/cqhttp.md +++ b/docs/api/adapters/cqhttp.md @@ -4,3 +4,439 @@ sidebarDepth: 0 --- # NoneBot.adapters.cqhttp 模块 + + +## `escape(s, *, escape_comma=True)` + + +* **说明** + + 对字符串进行 CQ 码转义。 + + + +* **参数** + + + * `s: str`: 需要转义的字符串 + + + * `escape_comma: bool`: 是否转义逗号(`,`)。 + + + +## `unescape(s)` + + +* **说明** + + 对字符串进行 CQ 码去转义。 + + + +* **参数** + + + * `s: str`: 需要转义的字符串 + + + +## _exception_ `CQHTTPAdapterException` + +基类:[`nonebot.exception.AdapterException`](../exception.md#nonebot.exception.AdapterException) + + +## _exception_ `ActionFailed` + +基类:[`nonebot.exception.ActionFailed`](../exception.md#nonebot.exception.ActionFailed), `nonebot.adapters.cqhttp.exception.CQHTTPAdapterException` + + +* **说明** + + API 请求返回错误信息。 + + + +* **参数** + + + * `retcode: Optional[int]`: 错误码 + + + +## _exception_ `NetworkError` + +基类:[`nonebot.exception.NetworkError`](../exception.md#nonebot.exception.NetworkError), `nonebot.adapters.cqhttp.exception.CQHTTPAdapterException` + + +* **说明** + + 网络错误。 + + + +* **参数** + + + * `retcode: Optional[int]`: 错误码 + + + +## _exception_ `ApiNotAvailable` + +基类:[`nonebot.exception.ApiNotAvailable`](../exception.md#nonebot.exception.ApiNotAvailable), `nonebot.adapters.cqhttp.exception.CQHTTPAdapterException` + + +## _async_ `_check_reply(bot, event)` + + +* **说明** + + 检查消息中存在的回复,去除并赋值 `event.reply`, `event.to_me` + + + +* **参数** + + + * `bot: Bot`: Bot 对象 + + + * `event: Event`: Event 对象 + + + +## `_check_at_me(bot, event)` + + +* **说明** + + 检查消息开头或结尾是否存在 @机器人,去除并赋值 `event.to_me` + + + +* **参数** + + + * `bot: Bot`: Bot 对象 + + + * `event: Event`: Event 对象 + + + +## `_check_nickname(bot, event)` + + +* **说明** + + 检查消息开头是否存在,去除并赋值 `event.to_me` + + + +* **参数** + + + * `bot: Bot`: Bot 对象 + + + * `event: Event`: Event 对象 + + + +## `_handle_api_result(result)` + + +* **说明** + + 处理 API 请求返回值。 + + + +* **参数** + + + * `result: Optional[Dict[str, Any]]`: API 返回数据 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +* **异常** + + + * `ActionFailed`: API 调用失败 + + + +## _class_ `Bot` + +基类:[`nonebot.adapters.BaseBot`](README.md#nonebot.adapters.BaseBot) + +CQHTTP 协议 Bot 适配。继承属性参考 [BaseBot](./#class-basebot) 。 + + +### _property_ `type` + + +* 返回: `"cqhttp"` + + +### _async classmethod_ `check_permission(driver, connection_type, headers, body)` + + +* **说明** + + CQHTTP (OneBot) 协议鉴权。参考 [鉴权](https://github.com/howmanybots/onebot/blob/master/v11/specs/communication/authorization.md) + + + +### _async_ `handle_message(message)` + + +* **说明** + + 调用 [_check_reply](#async-check-reply-bot-event), [_check_at_me](#check-at-me-bot-event), [_check_nickname](#check-nickname-bot-event) 处理事件并转换为 [Event](#class-event) + + + +### _async_ `call_api(api, **data)` + + +* **说明** + + 调用 CQHTTP 协议 API + + + +* **参数** + + + * `api: str`: API 名称 + + + * `**data: Any`: API 参数 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +* **异常** + + + * `NetworkError`: 网络错误 + + + * `ActionFailed`: API 调用失败 + + + +### _async_ `send(event, message, at_sender=False, **kwargs)` + + +* **说明** + + 根据 `event` 向触发事件的主体发送消息。 + + + +* **参数** + + + * `event: Event`: Event 对象 + + + * `message: Union[str, Message, MessageSegment]`: 要发送的消息 + + + * `at_sender: bool`: 是否 @ 事件主体 + + + * `**kwargs`: 覆盖默认参数 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +* **异常** + + + * `ValueError`: 缺少 `user_id`, `group_id` + + + * `NetworkError`: 网络错误 + + + * `ActionFailed`: API 调用失败 + + + +## _class_ `Event` + +基类:[`nonebot.adapters.BaseEvent`](README.md#nonebot.adapters.BaseEvent) + +CQHTTP 协议 Event 适配。继承属性参考 [BaseEvent](./#class-baseevent) 。 + + +### _property_ `id` + + +* 类型: `Optional[int]` + + +* 说明: 事件/消息 ID + + +### _property_ `name` + + +* 类型: `str` + + +* 说明: 事件名称,由类型与 `.` 组合而成 + + +### _property_ `self_id` + + +* 类型: `str` + + +* 说明: 机器人自身 ID + + +### _property_ `time` + + +* 类型: `int` + + +* 说明: 事件发生时间 + + +### _property_ `type` + + +* 类型: `str` + + +* 说明: 事件类型 + + +### _property_ `detail_type` + + +* 类型: `str` + + +* 说明: 事件详细类型 + + +### _property_ `sub_type` + + +* 类型: `Optional[str]` + + +* 说明: 事件子类型 + + +### _property_ `user_id` + + +* 类型: `Optional[int]` + + +* 说明: 事件主体 ID + + +### _property_ `group_id` + + +* 类型: `Optional[int]` + + +* 说明: 事件主体群 ID + + +### _property_ `to_me` + + +* 类型: `Optional[bool]` + + +* 说明: 消息是否与机器人相关 + + +### _property_ `message` + + +* 类型: `Optional[Message]` + + +* 说明: 消息内容 + + +### _property_ `reply` + + +* 类型: `Optional[dict]` + + +* 说明: 回复消息详情 + + +### _property_ `raw_message` + + +* 类型: `Optional[str]` + + +* 说明: 原始消息 + + +### _property_ `plain_text` + + +* 类型: `Optional[str]` + + +* 说明: 纯文本消息内容 + + +### _property_ `sender` + + +* 类型: `Optional[dict]` + + +* 说明: 消息发送者信息 + + +## _class_ `MessageSegment` + +基类:[`nonebot.adapters.BaseMessageSegment`](README.md#nonebot.adapters.BaseMessageSegment) + +CQHTTP 协议 MessageSegment 适配。具体方法参考协议消息段类型或源码。 + + +## _class_ `Message` + +基类:[`nonebot.adapters.BaseMessage`](README.md#nonebot.adapters.BaseMessage) + +CQHTTP 协议 Message 适配。 diff --git a/docs_build/README.rst b/docs_build/README.rst index 9f40afa9..95ffcc2d 100644 --- a/docs_build/README.rst +++ b/docs_build/README.rst @@ -17,3 +17,4 @@ NoneBot Api Reference - `nonebot.drivers.fastapi `_ - `nonebot.adapters `_ - `nonebot.adapters.cqhttp `_ + - `nonebot.adapters.ding `_ diff --git a/docs_build/adapters/cqhttp.rst b/docs_build/adapters/cqhttp.rst index 3e63952b..4714296d 100644 --- a/docs_build/adapters/cqhttp.rst +++ b/docs_build/adapters/cqhttp.rst @@ -6,7 +6,28 @@ sidebarDepth: 0 NoneBot.adapters.cqhttp 模块 ============================ -.. automodule:: nonebot.adapters.cqhttp +.. automodule:: nonebot.adapters.cqhttp.utils + :members: + :show-inheritance: + +.. automodule:: nonebot.adapters.cqhttp.exception + :members: + :show-inheritance: + + +.. automodule:: nonebot.adapters.cqhttp.bot + :members: + :private-members: + :show-inheritance: + + +.. automodule:: nonebot.adapters.cqhttp.event + :members: + :private-members: + :show-inheritance: + + +.. automodule:: nonebot.adapters.cqhttp.message :members: :private-members: :show-inheritance: diff --git a/nonebot/adapters/cqhttp/__init__.py b/nonebot/adapters/cqhttp/__init__.py index 5de85a92..8271c1e6 100644 --- a/nonebot/adapters/cqhttp/__init__.py +++ b/nonebot/adapters/cqhttp/__init__.py @@ -10,7 +10,7 @@ CQHTTP (OneBot) v11 协议适配 https://github.com/howmanybots/onebot/blob/master/README.md """ -from .utils import log from .event import Event from .message import Message, MessageSegment -from .bot import Bot, _check_at_me, _check_nickname, _check_reply +from .utils import log, escape, unescape, _b2s +from .bot import Bot, _check_at_me, _check_nickname, _check_reply, _handle_api_result diff --git a/nonebot/adapters/cqhttp/bot.py b/nonebot/adapters/cqhttp/bot.py index 5c7ac7a4..af673ec9 100644 --- a/nonebot/adapters/cqhttp/bot.py +++ b/nonebot/adapters/cqhttp/bot.py @@ -10,14 +10,24 @@ from nonebot.log import logger from nonebot.config import Config from nonebot.adapters import BaseBot from nonebot.message import handle_event +from nonebot.exception import RequestDenied from nonebot.typing import Any, Dict, Union, Optional from nonebot.typing import overrides, Driver, WebSocket, NoReturn -from nonebot.exception import NetworkError, RequestDenied, ApiNotAvailable from .event import Event -from .exception import ApiError -from .utils import log, get_auth_bearer from .message import Message, MessageSegment +from .exception import NetworkError, ApiNotAvailable, ActionFailed +from .utils import log + + +def get_auth_bearer( + access_token: Optional[str] = None) -> Union[Optional[str], NoReturn]: + if not access_token: + return None + scheme, _, param = access_token.partition(" ") + if scheme.lower() not in ["bearer", "token"]: + raise RequestDenied(401, "Not authenticated") + return param async def _check_reply(bot: "Bot", event: "Event"): @@ -160,11 +170,11 @@ def _handle_api_result( :异常: - - ``ApiError``: API 调用失败 + - ``ActionFailed``: API 调用失败 """ if isinstance(result, dict): if result.get("status") == "failed": - raise ApiError(retcode=result.get("retcode")) + raise ActionFailed(retcode=result.get("retcode")) return result.get("data") @@ -318,7 +328,7 @@ class Bot(BaseBot): :异常: - ``NetworkError``: 网络错误 - - ``ApiError``: API 调用失败 + - ``ActionFailed``: API 调用失败 """ if "self_id" in data: self_id = data.pop("self_id") @@ -393,7 +403,7 @@ class Bot(BaseBot): - ``ValueError``: 缺少 ``user_id``, ``group_id`` - ``NetworkError``: 网络错误 - - ``ApiError``: API 调用失败 + - ``ActionFailed``: API 调用失败 """ msg = message if isinstance(message, Message) else Message(message) diff --git a/nonebot/adapters/cqhttp/__init__.pyi b/nonebot/adapters/cqhttp/bot.pyi similarity index 78% rename from nonebot/adapters/cqhttp/__init__.pyi rename to nonebot/adapters/cqhttp/bot.pyi index e5398588..745b4941 100644 --- a/nonebot/adapters/cqhttp/__init__.pyi +++ b/nonebot/adapters/cqhttp/bot.pyi @@ -1,12 +1,11 @@ import asyncio from nonebot.config import Config -from nonebot.adapters import BaseBot, BaseEvent, BaseMessage, BaseMessageSegment -from nonebot.typing import Any, Dict, List, Union, Driver, Optional, NoReturn, WebSocket, Iterable +from nonebot.adapters import BaseBot +from nonebot.typing import Any, Dict, List, Union, Driver, Optional, NoReturn, WebSocket - -def log(level: str, message: str): - ... +from .event import Event +from .message import Message, MessageSegment def get_auth_bearer( @@ -14,27 +13,15 @@ def get_auth_bearer( ... -def escape(s: str, *, escape_comma: bool = ...) -> str: +async def _check_reply(bot: "Bot", event: Event): ... -def unescape(s: str) -> str: +def _check_at_me(bot: "Bot", event: Event): ... -def _b2s(b: Optional[bool]) -> Optional[str]: - ... - - -async def _check_reply(bot: "Bot", event: "Event"): - ... - - -def _check_at_me(bot: "Bot", event: "Event"): - ... - - -def _check_nickname(bot: "Bot", event: "Event"): +def _check_nickname(bot: "Bot", event: Event): ... @@ -86,8 +73,8 @@ class Bot(BaseBot): async def call_api(self, api: str, **data) -> Union[Any, NoReturn]: ... - async def send(self, event: "Event", message: Union[str, "Message", - "MessageSegment"], + async def send(self, event: Event, message: Union[str, Message, + MessageSegment], **kwargs) -> Union[Any, NoReturn]: ... @@ -759,242 +746,3 @@ class Bot(BaseBot): * ``self_id``: 机器人 QQ 号 """ ... - - -class Event(BaseEvent): - - def __init__(self, raw_event: dict): - ... - - @property - def id(self) -> Optional[int]: - ... - - @property - def name(self) -> str: - ... - - @property - def self_id(self) -> str: - ... - - @property - def time(self) -> int: - ... - - @property - def type(self) -> str: - ... - - @type.setter - def type(self, value) -> None: - ... - - @property - def detail_type(self) -> str: - ... - - @detail_type.setter - def detail_type(self, value) -> None: - ... - - @property - def sub_type(self) -> Optional[str]: - ... - - @sub_type.setter - def sub_type(self, value) -> None: - ... - - @property - def user_id(self) -> Optional[int]: - ... - - @user_id.setter - def user_id(self, value) -> None: - ... - - @property - def group_id(self) -> Optional[int]: - ... - - @group_id.setter - def group_id(self, value) -> None: - ... - - @property - def to_me(self) -> Optional[bool]: - ... - - @to_me.setter - def to_me(self, value) -> None: - ... - - @property - def message(self) -> Optional["Message"]: - ... - - @message.setter - def message(self, value) -> None: - ... - - @property - def reply(self) -> Optional[dict]: - ... - - @reply.setter - def reply(self, value) -> None: - ... - - @property - def raw_message(self) -> Optional[str]: - ... - - @raw_message.setter - def raw_message(self, value) -> None: - ... - - @property - def plain_text(self) -> Optional[str]: - ... - - @property - def sender(self) -> Optional[dict]: - ... - - @sender.setter - def sender(self, value) -> None: - ... - - -class MessageSegment(BaseMessageSegment): - - def __init__(self, type: str, data: Dict[str, Any]) -> None: - ... - - def __str__(self): - ... - - def __add__(self, other) -> "Message": - ... - - @staticmethod - def anonymous(ignore_failure: Optional[bool] = ...) -> "MessageSegment": - ... - - @staticmethod - def at(user_id: Union[int, str]) -> "MessageSegment": - ... - - @staticmethod - def contact_group(group_id: int) -> "MessageSegment": - ... - - @staticmethod - def contact_user(user_id: int) -> "MessageSegment": - ... - - @staticmethod - def dice() -> "MessageSegment": - ... - - @staticmethod - def face(id_: int) -> "MessageSegment": - ... - - @staticmethod - def forward(id_: str) -> "MessageSegment": - ... - - @staticmethod - def image(file: str, - type_: Optional[str] = ..., - cache: bool = ..., - proxy: bool = ..., - timeout: Optional[int] = ...) -> "MessageSegment": - ... - - @staticmethod - def json(data: str) -> "MessageSegment": - ... - - @staticmethod - def location(latitude: float, - longitude: float, - title: Optional[str] = ..., - content: Optional[str] = ...) -> "MessageSegment": - ... - - @staticmethod - def music(type_: str, id_: int) -> "MessageSegment": - ... - - @staticmethod - def music_custom(url: str, - audio: str, - title: str, - content: Optional[str] = ..., - img_url: Optional[str] = ...) -> "MessageSegment": - ... - - @staticmethod - def node(id_: int) -> "MessageSegment": - ... - - @staticmethod - def node_custom(user_id: int, nickname: str, - content: Union[str, "Message"]) -> "MessageSegment": - ... - - @staticmethod - def poke(type_: str, id_: str) -> "MessageSegment": - ... - - @staticmethod - def record(file: str, - magic: Optional[bool] = ..., - cache: Optional[bool] = ..., - proxy: Optional[bool] = ..., - timeout: Optional[int] = ...) -> "MessageSegment": - ... - - @staticmethod - def reply(id_: int) -> "MessageSegment": - ... - - @staticmethod - def rps() -> "MessageSegment": - ... - - @staticmethod - def shake() -> "MessageSegment": - ... - - @staticmethod - def share(url: str = ..., - title: str = ..., - content: Optional[str] = ..., - img_url: Optional[str] = ...) -> "MessageSegment": - ... - - @staticmethod - def text(text: str) -> "MessageSegment": - ... - - @staticmethod - def video(file: str, - cache: Optional[bool] = ..., - proxy: Optional[bool] = ..., - timeout: Optional[int] = ...) -> "MessageSegment": - ... - - @staticmethod - def xml(data: str) -> "MessageSegment": - ... - - -class Message(BaseMessage): - - @staticmethod - def _construct(msg: Union[str, dict, list]) -> Iterable[MessageSegment]: - ... diff --git a/nonebot/adapters/cqhttp/event.py b/nonebot/adapters/cqhttp/event.py index f3071151..5bc959c9 100644 --- a/nonebot/adapters/cqhttp/event.py +++ b/nonebot/adapters/cqhttp/event.py @@ -1,6 +1,5 @@ -from nonebot.typing import overrides -from nonebot.typing import Optional from nonebot.adapters import BaseEvent +from nonebot.typing import Optional, overrides from .message import Message diff --git a/nonebot/adapters/cqhttp/exception.py b/nonebot/adapters/cqhttp/exception.py index 86fe963c..2bcc73f4 100644 --- a/nonebot/adapters/cqhttp/exception.py +++ b/nonebot/adapters/cqhttp/exception.py @@ -1,4 +1,8 @@ -from nonebot.exception import AdapterException, ActionFailed +from nonebot.typing import Optional +from nonebot.exception import (AdapterException, ActionFailed as + BaseActionFailed, NetworkError as + BaseNetworkError, ApiNotAvailable as + BaseApiNotAvailable) class CQHTTPAdapterException(AdapterException): @@ -7,7 +11,7 @@ class CQHTTPAdapterException(AdapterException): super().__init__("cqhttp") -class ApiError(CQHTTPAdapterException, ActionFailed): +class ActionFailed(BaseActionFailed, CQHTTPAdapterException): """ :说明: @@ -27,3 +31,29 @@ class ApiError(CQHTTPAdapterException, ActionFailed): def __str__(self): return self.__repr__() + + +class NetworkError(BaseNetworkError, CQHTTPAdapterException): + """ + :说明: + + 网络错误。 + + :参数: + + * ``retcode: Optional[int]``: 错误码 + """ + + def __init__(self, msg: Optional[str] = None): + super().__init__() + self.msg = msg + + def __repr__(self): + return f"" + + def __str__(self): + return self.__repr__() + + +class ApiNotAvailable(BaseApiNotAvailable, CQHTTPAdapterException): + pass diff --git a/nonebot/adapters/cqhttp/message.py b/nonebot/adapters/cqhttp/message.py index b9f317e0..f411aa2b 100644 --- a/nonebot/adapters/cqhttp/message.py +++ b/nonebot/adapters/cqhttp/message.py @@ -3,6 +3,7 @@ import re from nonebot.typing import overrides from nonebot.typing import Any, Dict, Union, Tuple, Iterable, Optional from nonebot.adapters import BaseMessage, BaseMessageSegment + from .utils import log, escape, unescape, _b2s diff --git a/nonebot/adapters/cqhttp/utils.py b/nonebot/adapters/cqhttp/utils.py index 40661891..ecfee872 100644 --- a/nonebot/adapters/cqhttp/utils.py +++ b/nonebot/adapters/cqhttp/utils.py @@ -1,21 +1,9 @@ -from nonebot.typing import NoReturn -from nonebot.typing import Union, Optional -from nonebot.exception import RequestDenied +from nonebot.typing import Optional from nonebot.utils import logger_wrapper log = logger_wrapper("CQHTTP") -def get_auth_bearer( - access_token: Optional[str] = None) -> Union[Optional[str], NoReturn]: - if not access_token: - return None - scheme, _, param = access_token.partition(" ") - if scheme.lower() not in ["bearer", "token"]: - raise RequestDenied(401, "Not authenticated") - return param - - def escape(s: str, *, escape_comma: bool = True) -> str: """ :说明: diff --git a/nonebot/exception.py b/nonebot/exception.py index b97c3f08..6a4b65dc 100644 --- a/nonebot/exception.py +++ b/nonebot/exception.py @@ -6,8 +6,6 @@ 这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。 """ -from nonebot.typing import Optional - class NoneBotException(Exception): """ From afd01796aa48c60903b2d6166121f363d7677d23 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Thu, 3 Dec 2020 17:08:16 +0800 Subject: [PATCH 13/16] :construction: update ding adapter --- docs/api/adapters/ding.md | 374 +++++++++++++++++++++++++++- docs/api/utils.md | 2 - docs_build/adapters/ding.rst | 19 +- docs_build/utils.rst | 1 + nonebot/adapters/cqhttp/__init__.py | 1 + nonebot/adapters/cqhttp/bot.py | 8 +- nonebot/adapters/ding/__init__.py | 4 +- nonebot/adapters/ding/bot.py | 76 +++--- nonebot/adapters/ding/event.py | 9 +- nonebot/adapters/ding/exception.py | 46 +++- nonebot/adapters/ding/utils.py | 32 --- nonebot/utils.py | 23 +- 12 files changed, 496 insertions(+), 99 deletions(-) diff --git a/docs/api/adapters/ding.md b/docs/api/adapters/ding.md index 10d0aa42..0bbdd6d8 100644 --- a/docs/api/adapters/ding.md +++ b/docs/api/adapters/ding.md @@ -5,6 +5,376 @@ sidebarDepth: 0 # NoneBot.adapters.ding 模块 -## 钉钉群机器人 协议适配 -协议详情请看: [钉钉文档](https://ding-doc.dingtalk.com/doc#/serverapi2/krgddi) +## _exception_ `DingAdapterException` + +基类:[`nonebot.exception.AdapterException`](../exception.md#nonebot.exception.AdapterException) + + +* **说明** + + 钉钉 Adapter 错误基类 + + + +## _exception_ `ActionFailed` + +基类:[`nonebot.exception.ActionFailed`](../exception.md#nonebot.exception.ActionFailed), `nonebot.adapters.ding.exception.DingAdapterException` + + +* **说明** + + API 请求返回错误信息。 + + + +* **参数** + + + * `errcode: Optional[int]`: 错误码 + + + * `errmsg: Optional[str]`: 错误信息 + + + +## _exception_ `ApiNotAvailable` + +基类:[`nonebot.exception.ApiNotAvailable`](../exception.md#nonebot.exception.ApiNotAvailable), `nonebot.adapters.ding.exception.DingAdapterException` + + +## _exception_ `NetworkError` + +基类:[`nonebot.exception.NetworkError`](../exception.md#nonebot.exception.NetworkError), `nonebot.adapters.ding.exception.DingAdapterException` + + +* **说明** + + 网络错误。 + + + +* **参数** + + + * `retcode: Optional[int]`: 错误码 + + + +## _exception_ `SessionExpired` + +基类:[`nonebot.exception.ApiNotAvailable`](../exception.md#nonebot.exception.ApiNotAvailable), `nonebot.adapters.ding.exception.DingAdapterException` + + +* **说明** + + 发消息的 session 已经过期。 + + + +## _class_ `Bot` + +基类:[`nonebot.adapters.BaseBot`](README.md#nonebot.adapters.BaseBot) + +钉钉 协议 Bot 适配。继承属性参考 [BaseBot](./#class-basebot) 。 + + +### _property_ `type` + + +* 返回: `"ding"` + + +### _async classmethod_ `check_permission(driver, connection_type, headers, body)` + + +* **说明** + + 钉钉协议鉴权。参考 [鉴权](https://ding-doc.dingtalk.com/doc#/serverapi2/elzz1p) + + + +### _async_ `handle_message(body)` + + +* **说明** + + 处理上报消息的函数,转换为 `Event` 事件后调用 `nonebot.message.handle_event` 进一步处理事件。 + + + +* **参数** + + + * `message: dict`: 收到的上报消息 + + + +### _async_ `call_api(api, event=None, **data)` + + +* **说明** + + 调用 钉钉 协议 API + + + +* **参数** + + + * `api: str`: API 名称 + + + * `**data: Any`: API 参数 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +* **异常** + + + * `NetworkError`: 网络错误 + + + * `ActionFailed`: API 调用失败 + + + +### _async_ `send(event, message, at_sender=False, **kwargs)` + + +* **说明** + + 根据 `event` 向触发事件的主体发送消息。 + + + +* **参数** + + + * `event: Event`: Event 对象 + + + * `message: Union[str, Message, MessageSegment]`: 要发送的消息 + + + * `at_sender: bool`: 是否 @ 事件主体 + + + * `**kwargs`: 覆盖默认参数 + + + +* **返回** + + + * `Any`: API 调用返回数据 + + + +* **异常** + + + * `ValueError`: 缺少 `user_id`, `group_id` + + + * `NetworkError`: 网络错误 + + + * `ActionFailed`: API 调用失败 + + + +## _class_ `Event` + +基类:[`nonebot.adapters.BaseEvent`](README.md#nonebot.adapters.BaseEvent) + +钉钉 协议 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` + + +* 类型: `str` + + +* 说明: 事件类型 + + +### _property_ `detail_type` + + +* 类型: `str` + + +* 说明: 事件详细类型 + + +### _property_ `sub_type` + + +* 类型: `None` + + +* 说明: 钉钉适配器无事件子类型 + + +### _property_ `user_id` + + +* 类型: `Optional[str]` + + +* 说明: 发送者 ID + + +### _property_ `group_id` + + +* 类型: `Optional[str]` + + +* 说明: 事件主体群 ID + + +### _property_ `to_me` + + +* 类型: `Optional[bool]` + + +* 说明: 消息是否与机器人相关 + + +### _property_ `message` + + +* 类型: `Optional[Message]` + + +* 说明: 消息内容 + + +### _property_ `reply` + + +* 类型: `None` + + +* 说明: 回复消息详情 + + +### _property_ `raw_message` + + +* 类型: `Optional[str]` + + +* 说明: 原始消息 + + +### _property_ `plain_text` + + +* 类型: `Optional[str]` + + +* 说明: 纯文本消息内容 + + +### _property_ `sender` + + +* 类型: `Optional[dict]` + + +* 说明: 消息发送者信息 + + +## _class_ `MessageSegment` + +基类:[`nonebot.adapters.BaseMessageSegment`](README.md#nonebot.adapters.BaseMessageSegment) + +钉钉 协议 MessageSegment 适配。具体方法参考协议消息段类型或源码。 + + +### _static_ `actionCardSingleMultiBtns(title, text, btns=[], hideAvatar=False, btnOrientation='1')` + + +* **参数** + + + * `btnOrientation`: 0:按钮竖直排列 1:按钮横向排列 + + + * `btns`: [{ "title": title, "actionURL": actionURL }, ...] + + + +### _static_ `feedCard(links=[])` + + +* **参数** + + + * `links`: [{ "title": xxx, "messageURL": xxx, "picURL": xxx }, ...] + + + +### _static_ `empty()` + +不想回复消息到群里 + + +## _class_ `Message` + +基类:[`nonebot.adapters.BaseMessage`](README.md#nonebot.adapters.BaseMessage) + +钉钉 协议 Message 适配。 diff --git a/docs/api/utils.md b/docs/api/utils.md index ed98fab9..52cf5766 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -54,8 +54,6 @@ sidebarDepth: 0 ## _class_ `DataclassEncoder` -基类:`json.encoder.JSONEncoder` - * **说明** diff --git a/docs_build/adapters/ding.rst b/docs_build/adapters/ding.rst index b26dac19..cff5979f 100644 --- a/docs_build/adapters/ding.rst +++ b/docs_build/adapters/ding.rst @@ -6,7 +6,24 @@ sidebarDepth: 0 NoneBot.adapters.ding 模块 ============================ -.. automodule:: nonebot.adapters.ding +.. automodule:: nonebot.adapters.ding.exception + :members: + :show-inheritance: + + +.. automodule:: nonebot.adapters.ding.bot + :members: + :private-members: + :show-inheritance: + + +.. automodule:: nonebot.adapters.ding.event + :members: + :private-members: + :show-inheritance: + + +.. automodule:: nonebot.adapters.ding.message :members: :private-members: :show-inheritance: diff --git a/docs_build/utils.rst b/docs_build/utils.rst index b7609fdc..5e7acd8d 100644 --- a/docs_build/utils.rst +++ b/docs_build/utils.rst @@ -10,4 +10,5 @@ NoneBot.utils 模块 .. autofunction:: nonebot.utils.escape_tag .. autodecorator:: nonebot.utils.run_sync .. autoclass:: nonebot.utils.DataclassEncoder +.. autodecorator:: nonebot.utils.logger_wrapper :show-inheritance: diff --git a/nonebot/adapters/cqhttp/__init__.py b/nonebot/adapters/cqhttp/__init__.py index 8271c1e6..14635eda 100644 --- a/nonebot/adapters/cqhttp/__init__.py +++ b/nonebot/adapters/cqhttp/__init__.py @@ -14,3 +14,4 @@ from .event import Event from .message import Message, MessageSegment from .utils import log, escape, unescape, _b2s from .bot import Bot, _check_at_me, _check_nickname, _check_reply, _handle_api_result +from .exception import CQHTTPAdapterException, ApiNotAvailable, ActionFailed, NetworkError diff --git a/nonebot/adapters/cqhttp/bot.py b/nonebot/adapters/cqhttp/bot.py index af673ec9..929cd095 100644 --- a/nonebot/adapters/cqhttp/bot.py +++ b/nonebot/adapters/cqhttp/bot.py @@ -247,7 +247,7 @@ class Bot(BaseBot): """ x_self_id = headers.get("x-self-id") x_signature = headers.get("x-signature") - access_token = get_auth_bearer(headers.get("authorization")) + token = get_auth_bearer(headers.get("authorization")) # 检查连接方式 if connection_type not in ["http", "websocket"]: @@ -272,13 +272,13 @@ class Bot(BaseBot): raise RequestDenied(403, "Signature is invalid") access_token = driver.config.access_token - if access_token and access_token != access_token: + if access_token and access_token != token: log( "WARNING", "Authorization Header is invalid" - if access_token else "Missing Authorization Header") + if token else "Missing Authorization Header") raise RequestDenied( 403, "Authorization Header is invalid" - if access_token else "Missing Authorization Header") + if token else "Missing Authorization Header") return str(x_self_id) @overrides(BaseBot) diff --git a/nonebot/adapters/ding/__init__.py b/nonebot/adapters/ding/__init__.py index e9742bf4..4eb33e28 100644 --- a/nonebot/adapters/ding/__init__.py +++ b/nonebot/adapters/ding/__init__.py @@ -9,7 +9,9 @@ """ +from .utils import log from .bot import Bot from .event import Event from .message import Message, MessageSegment -from .exception import ApiError, SessionExpired, DingAdapterException +from .exception import (DingAdapterException, ApiNotAvailable, NetworkError, + ActionFailed, SessionExpired) diff --git a/nonebot/adapters/ding/bot.py b/nonebot/adapters/ding/bot.py index 97ff7c2e..0f0a10a8 100644 --- a/nonebot/adapters/ding/bot.py +++ b/nonebot/adapters/ding/bot.py @@ -1,19 +1,20 @@ -import httpx +import hmac +import base64 from datetime import datetime +import httpx from nonebot.log import logger from nonebot.config import Config from nonebot.adapters import BaseBot from nonebot.message import handle_event -from nonebot.typing import Driver, NoReturn -from nonebot.typing import Any, Union, Optional -from nonebot.exception import NetworkError, RequestDenied, ApiNotAvailable +from nonebot.exception import RequestDenied +from nonebot.typing import Any, Union, Driver, Optional, NoReturn +from .utils import log from .event import Event from .model import MessageModel -from .utils import check_legal, log from .message import Message, MessageSegment -from .exception import ApiError, SessionExpired +from .exception import NetworkError, ApiNotAvailable, ActionFailed, SessionExpired class Bot(BaseBot): @@ -35,8 +36,7 @@ class Bot(BaseBot): @classmethod async def check_permission(cls, driver: Driver, connection_type: str, - headers: dict, - body: Optional[dict]) -> Union[str, NoReturn]: + headers: dict, body: Optional[dict]) -> str: """ :说明: @@ -45,25 +45,29 @@ class Bot(BaseBot): timestamp = headers.get("timestamp") sign = headers.get("sign") - # 检查 timestamp - if not timestamp: - raise RequestDenied(400, "Missing `timestamp` Header") - # 检查 sign - if not sign: - raise RequestDenied(400, "Missing `sign` Header") - # 校验 sign 和 timestamp,判断是否是来自钉钉的合法请求 - if not check_legal(timestamp, sign, driver): - raise RequestDenied(403, "Signature is invalid") # 检查连接方式 if connection_type not in ["http"]: raise RequestDenied(405, "Unsupported connection type") - access_token = driver.config.access_token - if access_token and access_token != access_token: - raise RequestDenied( - 403, "Authorization Header is invalid" - if access_token else "Missing Authorization Header") - return body.get("chatbotUserId") + # 检查 timestamp + if not timestamp: + raise RequestDenied(400, "Missing `timestamp` Header") + + # 检查 sign + secret = driver.config.secret + if secret: + if not sign: + log("WARNING", "Missing Signature Header") + raise RequestDenied(400, "Missing `sign` Header") + string_to_sign = f"{timestamp}\n{secret}" + sig = hmac.new(secret.encode("utf-8"), + string_to_sign.encode("utf-8"), "sha256").digest() + if sign != base64.b64encode(sig).decode("utf-8"): + log("WARNING", "Signature Header is invalid") + raise RequestDenied(403, "Signature is invalid") + else: + log("WARNING", "Ding signature check ignored!") + return body["chatbotUserId"] async def handle_message(self, body: dict): message = MessageModel.parse_obj(body) @@ -79,7 +83,10 @@ class Bot(BaseBot): ) return - async def call_api(self, api: str, **data) -> Union[Any, NoReturn]: + async def call_api(self, + api: str, + event: Optional[Event] = None, + **data) -> Union[Any, NoReturn]: """ :说明: @@ -111,13 +118,15 @@ class Bot(BaseBot): log("DEBUG", f"Calling API {api}") if api == "send_message": - raw_event: MessageModel = data["raw_event"] - # 确保 sessionWebhook 没有过期 - if int(datetime.now().timestamp()) > int( - raw_event.sessionWebhookExpiredTime / 1000): - raise SessionExpired + if event: + # 确保 sessionWebhook 没有过期 + if int(datetime.now().timestamp()) > int( + event.raw_event.sessionWebhookExpiredTime / 1000): + raise SessionExpired - target = raw_event.sessionWebhook + target = event.raw_event.sessionWebhook + else: + target = None if not target: raise ApiNotAvailable @@ -136,8 +145,8 @@ class Bot(BaseBot): result = response.json() if isinstance(result, dict): if result.get("errcode") != 0: - raise ApiError(errcode=result.get("errcode"), - errmsg=result.get("errmsg")) + raise ActionFailed(errcode=result.get("errcode"), + errmsg=result.get("errmsg")) return result raise NetworkError(f"HTTP request received unexpected " f"status code: {response.status_code}") @@ -176,7 +185,8 @@ class Bot(BaseBot): msg = message if isinstance(message, Message) else Message(message) at_sender = at_sender and bool(event.user_id) - params = {"raw_event": event.raw_event} + params = {} + params["event"] = event params.update(kwargs) if at_sender and event.detail_type != "private": diff --git a/nonebot/adapters/ding/event.py b/nonebot/adapters/ding/event.py index 9c9fb50f..876ad493 100644 --- a/nonebot/adapters/ding/event.py +++ b/nonebot/adapters/ding/event.py @@ -1,6 +1,5 @@ -from typing import Literal, Union, Optional - from nonebot.adapters import BaseEvent +from nonebot.typing import Union, Optional from .message import Message from .model import MessageModel, ConversationType, TextMessage @@ -67,7 +66,7 @@ class Event(BaseEvent): pass @property - def detail_type(self) -> Literal["private", "group"]: + def detail_type(self) -> str: """ - 类型: ``str`` - 说明: 事件详细类型 @@ -125,10 +124,6 @@ class Event(BaseEvent): """ return self.detail_type == "private" or self.raw_event.isInAtList - @to_me.setter - def to_me(self, value) -> None: - pass - @property def message(self) -> Optional["Message"]: """ diff --git a/nonebot/adapters/ding/exception.py b/nonebot/adapters/ding/exception.py index ad6f4a20..b1d74d14 100644 --- a/nonebot/adapters/ding/exception.py +++ b/nonebot/adapters/ding/exception.py @@ -1,4 +1,8 @@ -from nonebot.exception import AdapterException, ActionFailed, ApiNotAvailable +from nonebot.typing import Optional +from nonebot.exception import (AdapterException, ActionFailed as + BaseActionFailed, ApiNotAvailable as + BaseApiNotAvailable, NetworkError as + BaseNetworkError) class DingAdapterException(AdapterException): @@ -6,22 +10,27 @@ class DingAdapterException(AdapterException): :说明: 钉钉 Adapter 错误基类 - """ def __init__(self) -> None: super().__init__("ding") -class ApiError(DingAdapterException, ActionFailed): +class ActionFailed(BaseActionFailed, DingAdapterException): """ :说明: API 请求返回错误信息。 + :参数: + + * ``errcode: Optional[int]``: 错误码 + * ``errmsg: Optional[str]``: 错误信息 """ - def __init__(self, errcode: int, errmsg: str): + def __init__(self, + errcode: Optional[int] = None, + errmsg: Optional[str] = None): super().__init__() self.errcode = errcode self.errmsg = errmsg @@ -30,12 +39,37 @@ class ApiError(DingAdapterException, ActionFailed): return f"" -class SessionExpired(DingAdapterException, ApiNotAvailable): +class ApiNotAvailable(BaseApiNotAvailable, DingAdapterException): + pass + + +class NetworkError(BaseNetworkError, DingAdapterException): + """ + :说明: + + 网络错误。 + + :参数: + + * ``retcode: Optional[int]``: 错误码 + """ + + def __init__(self, msg: Optional[str] = None): + super().__init__() + self.msg = msg + + def __repr__(self): + return f"" + + def __str__(self): + return self.__repr__() + + +class SessionExpired(BaseApiNotAvailable, DingAdapterException): """ :说明: 发消息的 session 已经过期。 - """ def __repr__(self) -> str: diff --git a/nonebot/adapters/ding/utils.py b/nonebot/adapters/ding/utils.py index 8c644683..eb4145bc 100644 --- a/nonebot/adapters/ding/utils.py +++ b/nonebot/adapters/ding/utils.py @@ -1,35 +1,3 @@ -import base64 -import hashlib -import hmac -from typing import TYPE_CHECKING - from nonebot.utils import logger_wrapper -if TYPE_CHECKING: - from nonebot.drivers import BaseDriver log = logger_wrapper("DING") - - -def check_legal(timestamp, remote_sign, driver: "BaseDriver"): - """ - 1. timestamp 与系统当前时间戳如果相差1小时以上,则认为是非法的请求。 - - 2. sign 与开发者自己计算的结果不一致,则认为是非法的请求。 - - 必须当timestamp和sign同时验证通过,才能认为是来自钉钉的合法请求。 - """ - # 目前先设置成 secret - # TODO 后面可能可以从 secret[adapter_name] 获取 - app_secret = driver.config.secret # 机器人的 appSecret - if not app_secret: - # TODO warning - log("WARNING", "No ding secrets set, won't check sign") - return True - app_secret_enc = app_secret.encode('utf-8') - string_to_sign = '{}\n{}'.format(timestamp, app_secret) - string_to_sign_enc = string_to_sign.encode('utf-8') - hmac_code = hmac.new(app_secret_enc, - string_to_sign_enc, - digestmod=hashlib.sha256).digest() - sign = base64.b64encode(hmac_code).decode('utf-8') - return remote_sign == sign diff --git a/nonebot/utils.py b/nonebot/utils.py index 7e59e2aa..7ef93769 100644 --- a/nonebot/utils.py +++ b/nonebot/utils.py @@ -5,7 +5,7 @@ import dataclasses from functools import wraps, partial from nonebot.log import logger -from nonebot.typing import Any, Callable, Awaitable, overrides +from nonebot.typing import Any, Optional, Callable, Awaitable, overrides def escape_tag(s: str) -> str: @@ -65,19 +65,20 @@ class DataclassEncoder(json.JSONEncoder): def logger_wrapper(logger_name: str): + """ + :说明: - def log(level: str, message: str): - """ - :说明: + 用于打印 adapter 的日志。 - 用于打印 adapter 的日志。 + :log 参数: - :参数: + * ``level: Literal['WARNING', 'DEBUG', 'INFO']``: 日志等级 + * ``message: str``: 日志信息 + * ``exception: Optional[Exception]``: 异常信息 + """ - * ``level: Literal['WARNING', 'DEBUG', 'INFO']``: 日志等级 - * ``message: str``: 日志信息 - """ - return logger.opt(colors=True).log(level, - f"{logger_name} | " + message) + def log(level: str, message: str, exception: Optional[Exception] = None): + return logger.opt(colors=True, exception=exception).log( + level, f"{logger_name} | " + message) return log From be244f4538e0eecacf2e6ff1902480367c660dc1 Mon Sep 17 00:00:00 2001 From: Artin Date: Thu, 3 Dec 2020 18:47:58 +0800 Subject: [PATCH 14/16] :bug: Fix `Message._construct` error --- nonebot/adapters/ding/message.py | 6 +++--- tests/test_plugins/test_permission.py | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/nonebot/adapters/ding/message.py b/nonebot/adapters/ding/message.py index 53b83f6e..d8e88314 100644 --- a/nonebot/adapters/ding/message.py +++ b/nonebot/adapters/ding/message.py @@ -45,8 +45,8 @@ class MessageSegment(BaseMessageSegment): self.data["at"]["isAtAll"] = value @staticmethod - def text(text: str) -> "MessageSegment": - return MessageSegment("text", {"text": {"content": text.strip()}}) + def text(text_: str) -> "MessageSegment": + return MessageSegment("text", {"text": {"content": text_.strip()}}) @staticmethod def markdown(title: str, text: str) -> "MessageSegment": @@ -130,4 +130,4 @@ class Message(BaseMessage): elif isinstance(msg, TextMessage): yield MessageSegment("text", {"text": msg.dict()}) elif isinstance(msg, str): - yield MessageSegment.text(str) + yield MessageSegment.text(msg) diff --git a/tests/test_plugins/test_permission.py b/tests/test_plugins/test_permission.py index ed03b80f..23d4e0f5 100644 --- a/tests/test_plugins/test_permission.py +++ b/tests/test_plugins/test_permission.py @@ -2,6 +2,7 @@ from nonebot.rule import to_me from nonebot.typing import Event from nonebot.plugin import on_startswith from nonebot.adapters.cqhttp import Bot +from nonebot.adapters.ding import Bot as DingBot, Event as DingEvent from nonebot.permission import GROUP_ADMIN test_command = on_startswith("hello", to_me(), permission=GROUP_ADMIN) @@ -9,4 +10,9 @@ test_command = on_startswith("hello", to_me(), permission=GROUP_ADMIN) @test_command.handle() async def test_handler(bot: Bot, event: Event, state: dict): - await test_command.finish("hello") + await test_command.finish("cqhttp hello") + + +@test_command.handle() +async def test_handler(bot: DingBot, event: DingEvent, state: dict): + await test_command.finish("ding hello") From 71c5cdc445de4741dee80daf743b2df8b3b4170a Mon Sep 17 00:00:00 2001 From: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> Date: Thu, 3 Dec 2020 19:12:46 +0800 Subject: [PATCH 15/16] :rewind: revert plain text --- 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 4d1fa60c..692c691f 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -453,4 +453,4 @@ class BaseMessage(list, abc.ABC): return f"{x} {y}" if y.type == "text" else x plain_text = reduce(_concat, self, "") - return plain_text.strip() + return plain_text[1:] if plain_text else plain_text From 0838cdfbe74faad33c8ad7df013175a2189ec71d Mon Sep 17 00:00:00 2001 From: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> Date: Thu, 3 Dec 2020 19:24:55 +0800 Subject: [PATCH 16/16] :art: improve format --- nonebot/adapters/cqhttp/message.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nonebot/adapters/cqhttp/message.py b/nonebot/adapters/cqhttp/message.py index f411aa2b..47d21bc8 100644 --- a/nonebot/adapters/cqhttp/message.py +++ b/nonebot/adapters/cqhttp/message.py @@ -1,7 +1,6 @@ import re -from nonebot.typing import overrides -from nonebot.typing import Any, Dict, Union, Tuple, Iterable, Optional +from nonebot.typing import Any, Dict, Union, Tuple, Iterable, Optional, overrides from nonebot.adapters import BaseMessage, BaseMessageSegment from .utils import log, escape, unescape, _b2s