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