nonebot2/nonebot/adapters/ding/bot.py

224 lines
7.4 KiB
Python
Raw Normal View History

2020-12-03 17:08:16 +08:00
import hmac
import base64
from datetime import datetime
2020-12-06 02:30:19 +08:00
from typing import Any, Union, Optional, TYPE_CHECKING
2020-12-03 00:59:32 +08:00
2020-12-03 17:08:16 +08:00
import httpx
2020-12-03 00:59:32 +08:00
from nonebot.log import logger
2020-12-30 00:36:29 +08:00
from nonebot.typing import overrides
2020-12-03 00:59:32 +08:00
from nonebot.message import handle_event
2020-12-07 00:06:09 +08:00
from nonebot.adapters import Bot as BaseBot
2020-12-03 17:08:16 +08:00
from nonebot.exception import RequestDenied
2020-12-03 17:08:16 +08:00
from .utils import log
2021-01-17 13:46:29 +08:00
from .config import Config as DingConfig
from .message import Message, MessageSegment
2020-12-03 17:08:16 +08:00
from .exception import NetworkError, ApiNotAvailable, ActionFailed, SessionExpired
2021-01-17 13:46:29 +08:00
from .event import MessageEvent, PrivateMessageEvent, GroupMessageEvent, ConversationType
2020-12-03 00:59:32 +08:00
2020-12-06 02:30:19 +08:00
if TYPE_CHECKING:
2021-01-17 13:46:29 +08:00
from nonebot.config import Config
2020-12-30 00:36:29 +08:00
from nonebot.drivers import Driver
2020-12-06 02:30:19 +08:00
SEND_BY_SESSION_WEBHOOK = "send_by_sessionWebhook"
2020-12-03 00:59:32 +08:00
class Bot(BaseBot):
"""
钉钉 协议 Bot 适配继承属性参考 `BaseBot <./#class-basebot>`_ 。
"""
2021-01-17 13:46:29 +08:00
ding_config: DingConfig
2020-12-03 00:59:32 +08:00
2021-01-17 13:46:29 +08:00
def __init__(self, connection_type: str, self_id: str, **kwargs):
2020-12-03 00:59:32 +08:00
2021-01-17 13:46:29 +08:00
super().__init__(connection_type, self_id, **kwargs)
2020-12-03 00:59:32 +08:00
@property
def type(self) -> str:
"""
- 返回: ``"ding"``
"""
return "ding"
2021-01-17 13:46:29 +08:00
@classmethod
def register(cls, driver: "Driver", config: "Config"):
super().register(driver, config)
cls.ding_config = DingConfig(**config.dict())
2020-12-03 00:59:32 +08:00
@classmethod
2020-12-30 00:36:29 +08:00
@overrides(BaseBot)
2020-12-06 02:30:19 +08:00
async def check_permission(cls, driver: "Driver", connection_type: str,
2020-12-03 17:08:16 +08:00
headers: dict, body: Optional[dict]) -> str:
2020-12-03 00:59:32 +08:00
"""
:说明:
2020-12-03 00:59:32 +08:00
钉钉协议鉴权参考 `鉴权 <https://ding-doc.dingtalk.com/doc#/serverapi2/elzz1p>`_
"""
timestamp = headers.get("timestamp")
sign = headers.get("sign")
# 检查连接方式
if connection_type not in ["http"]:
raise RequestDenied(
405, "Unsupported connection type, available type: `http`")
2020-12-03 00:59:32 +08:00
2020-12-03 17:08:16 +08:00
# 检查 timestamp
if not timestamp:
raise RequestDenied(400, "Missing `timestamp` Header")
# 检查 sign
2021-01-17 13:46:29 +08:00
secret = cls.ding_config.secret
2020-12-03 17:08:16 +08:00
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"]
2020-12-03 00:59:32 +08:00
2020-12-30 00:36:29 +08:00
@overrides(BaseBot)
async def handle_message(self, message: dict):
if not message:
return
# 判断消息类型,生成不同的 Event
2020-12-30 00:36:29 +08:00
try:
conversation_type = message["conversationType"]
if conversation_type == ConversationType.private:
event = PrivateMessageEvent.parse_obj(message)
elif conversation_type == ConversationType.group:
event = GroupMessageEvent.parse_obj(message)
else:
raise ValueError("Unsupported conversation type")
except Exception as e:
log("ERROR", "Event Parser Error", e)
2020-12-03 00:59:32 +08:00
return
try:
await handle_event(self, event)
except Exception as e:
logger.opt(colors=True, exception=e).error(
2020-12-30 20:08:22 +08:00
f"<r><bg #f8bbd0>Failed to handle event. Raw: {message}</bg #f8bbd0></r>"
2020-12-03 00:59:32 +08:00
)
return
2020-12-30 00:36:29 +08:00
@overrides(BaseBot)
2020-12-03 17:08:16 +08:00
async def call_api(self,
api: str,
event: Optional[MessageEvent] = None,
2020-12-05 20:32:38 +08:00
**data) -> Any:
2020-12-03 00:59:32 +08:00
"""
:说明:
调用 钉钉 协议 API
:参数:
* ``api: str``: API 名称
* ``**data: Any``: API 参数
:返回:
- ``Any``: API 调用返回数据
:异常:
- ``NetworkError``: 网络错误
- ``ActionFailed``: API 调用失败
"""
2020-12-03 12:08:04 +08:00
if self.connection_type != "http":
log("ERROR", "Only support http connection.")
return
2020-12-03 00:59:32 +08:00
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 <y>{api}</y>")
if api == SEND_BY_SESSION_WEBHOOK:
2020-12-03 17:08:16 +08:00
if event:
# 确保 sessionWebhook 没有过期
if int(datetime.now().timestamp()) > int(
event.sessionWebhookExpiredTime / 1000):
2020-12-03 17:08:16 +08:00
raise SessionExpired
2020-12-03 00:59:32 +08:00
target = event.sessionWebhook
2020-12-03 17:08:16 +08:00
else:
2020-12-03 00:59:32 +08:00
raise ApiNotAvailable
headers = {}
2020-12-30 00:36:29 +08:00
message: Message = data.get("message", None)
if not message:
raise ValueError("Message not found")
2020-12-03 00:59:32 +08:00
try:
async with httpx.AsyncClient(headers=headers) as client:
response = await client.post(
target,
2021-01-17 13:46:29 +08:00
params={"access_token": self.ding_config.access_token},
2020-12-30 00:36:29 +08:00
json=message._produce(),
2020-12-03 00:59:32 +08:00
timeout=self.config.api_timeout)
if 200 <= response.status_code < 300:
result = response.json()
if isinstance(result, dict):
if result.get("errcode") != 0:
2020-12-03 17:08:16 +08:00
raise ActionFailed(errcode=result.get("errcode"),
errmsg=result.get("errmsg"))
2020-12-03 00:59:32 +08:00
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")
2020-12-30 00:36:29 +08:00
@overrides(BaseBot)
2020-12-03 00:59:32 +08:00
async def send(self,
2020-12-30 00:36:29 +08:00
event: MessageEvent,
2020-12-03 00:59:32 +08:00
message: Union[str, "Message", "MessageSegment"],
at_sender: bool = False,
2020-12-05 20:32:38 +08:00
**kwargs) -> Any:
2020-12-03 00:59:32 +08:00
"""
:说明:
根据 ``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)
2020-12-30 00:36:29 +08:00
at_sender = at_sender and bool(event.senderId)
2020-12-03 17:08:16 +08:00
params = {}
params["event"] = event
2020-12-03 00:59:32 +08:00
params.update(kwargs)
2020-12-30 00:36:29 +08:00
if at_sender and event.conversationType != ConversationType.private:
params["message"] = f"@{event.senderNick} " + msg
2020-12-03 00:59:32 +08:00
else:
params["message"] = msg
return await self.call_api(SEND_BY_SESSION_WEBHOOK, **params)