nonebot2/packages/nonebot-adapter-ding/nonebot/adapters/ding/bot.py

251 lines
8.8 KiB
Python
Raw Normal View History

2021-03-20 14:49:58 +08:00
import json
import time
2021-10-21 00:37:02 +08:00
import urllib.parse
2021-06-10 21:52:20 +08:00
from datetime import datetime
2021-10-21 00:37:02 +08:00
from typing import TYPE_CHECKING, Any, Tuple, Union, Optional
2020-12-03 00:59:32 +08:00
2020-12-03 17:08:16 +08:00
import httpx
2021-06-10 21:52:20 +08:00
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
2021-10-21 00:37:02 +08:00
from nonebot.utils import escape_tag
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
2021-10-21 00:37:02 +08:00
from nonebot.drivers import Driver, HTTPRequest, HTTPResponse, HTTPConnection
2021-01-17 13:46:29 +08:00
from .config import Config as DingConfig
2021-10-21 00:37:02 +08:00
from .utils import log, calc_hmac_base64
from .message import Message, MessageSegment
2021-10-21 00:37:02 +08:00
from .exception import (ActionFailed, NetworkError, SessionExpired,
ApiNotAvailable)
from .event import (MessageEvent, ConversationType, GroupMessageEvent,
PrivateMessageEvent)
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-06 02:30:19 +08:00
SEND = "send"
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
@property
def type(self) -> str:
"""
- 返回: ``"ding"``
"""
return "ding"
2021-01-17 13:46:29 +08:00
@classmethod
2021-06-10 21:52:20 +08:00
def register(cls, driver: Driver, config: "Config"):
2021-01-17 13:46:29 +08:00
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)
2021-06-10 21:52:20 +08:00
async def check_permission(
cls, driver: Driver,
request: HTTPConnection) -> Tuple[Optional[str], HTTPResponse]:
2020-12-03 00:59:32 +08:00
"""
:说明:
2020-12-03 00:59:32 +08:00
钉钉协议鉴权参考 `鉴权 <https://ding-doc.dingtalk.com/doc#/serverapi2/elzz1p>`_
"""
2021-06-10 21:52:20 +08:00
timestamp = request.headers.get("timestamp")
sign = request.headers.get("sign")
2020-12-03 00:59:32 +08:00
# 检查连接方式
2021-06-10 21:52:20 +08:00
if not isinstance(request, HTTPRequest):
return None, HTTPResponse(
405, b"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:
2021-06-10 21:52:20 +08:00
return None, HTTPResponse(400, b"Missing `timestamp` Header")
2020-12-03 17:08:16 +08:00
# 检查 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")
2021-06-10 21:52:20 +08:00
return None, HTTPResponse(400, b"Missing `sign` Header")
sign_base64 = calc_hmac_base64(str(timestamp), secret)
if sign != sign_base64.decode('utf-8'):
2020-12-03 17:08:16 +08:00
log("WARNING", "Signature Header is invalid")
2021-06-10 21:52:20 +08:00
return None, HTTPResponse(403, b"Signature is invalid")
2020-12-03 17:08:16 +08:00
else:
log("WARNING", "Ding signature check ignored!")
2021-06-10 21:52:20 +08:00
return (json.loads(request.body.decode())["chatbotUserId"],
HTTPResponse(204, b''))
2020-12-03 00:59:32 +08:00
2020-12-30 00:36:29 +08:00
@overrides(BaseBot)
2021-06-10 21:52:20 +08:00
async def handle_message(self, message: bytes):
data: dict = json.loads(message)
2021-06-10 21:52:20 +08:00
if not data:
return
# 判断消息类型,生成不同的 Event
2020-12-30 00:36:29 +08:00
try:
2021-06-10 21:52:20 +08:00
conversation_type = data["conversationType"]
2020-12-30 00:36:29 +08:00
if conversation_type == ConversationType.private:
2021-06-10 21:52:20 +08:00
event = PrivateMessageEvent.parse_obj(data)
2020-12-30 00:36:29 +08:00
elif conversation_type == ConversationType.group:
2021-06-10 21:52:20 +08:00
event = GroupMessageEvent.parse_obj(data)
2020-12-30 00:36:29 +08:00
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(
f"<r><bg #f8bbd0>Failed to handle event. Raw: {escape_tag(str(data))}</bg #f8bbd0></r>"
2020-12-03 00:59:32 +08:00
)
return
2020-12-30 00:36:29 +08:00
@overrides(BaseBot)
2021-03-31 16:51:09 +08:00
async def _call_api(self,
api: str,
event: Optional[MessageEvent] = None,
**data) -> Any:
if not isinstance(self.request, HTTPRequest):
2020-12-03 12:08:04 +08:00
log("ERROR", "Only support http connection.")
return
2020-12-03 00:59:32 +08:00
log("DEBUG", f"Calling API <y>{api}</y>")
params = {}
# 传入参数有 webhook则使用传入的 webhook
webhook = data.get("webhook")
if webhook:
secret = data.get("secret")
if secret:
# 有这个参数的时候再计算加签的值
timestamp = str(round(time.time() * 1000))
params["timestamp"] = timestamp
hmac_code_base64 = calc_hmac_base64(timestamp, secret)
sign = urllib.parse.quote_plus(hmac_code_base64)
params["sign"] = sign
else:
# webhook 不存在则使用 event 中的 sessionWebhook
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
webhook = event.sessionWebhook
2020-12-03 17:08:16 +08:00
else:
2020-12-03 00:59:32 +08:00
raise ApiNotAvailable
headers = {}
message: Message = data.get("message", None)
if not message:
raise ValueError("Message not found")
try:
2021-10-21 00:37:02 +08:00
async with httpx.AsyncClient(headers=headers,
follow_redirects=True) as client:
response = await client.post(webhook,
params=params,
json=message._produce(),
timeout=self.config.api_timeout)
if 200 <= response.status_code < 300:
result = response.json()
if isinstance(result, dict):
if result.get("errcode") != 0:
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}")
except httpx.InvalidURL:
raise NetworkError("API root url invalid")
except httpx.HTTPError:
raise NetworkError("HTTP request failed")
2020-12-03 00:59:32 +08:00
2021-03-31 16:51:09 +08:00
@overrides(BaseBot)
async def call_api(self,
api: str,
event: Optional[MessageEvent] = None,
**data) -> Any:
"""
:说明:
调用 钉钉 协议 API
:参数:
* ``api: str``: API 名称
* ``event: Optional[MessageEvent]``: Event 对象
* ``**data: Any``: API 参数
:返回:
- ``Any``: API 调用返回数据
:异常:
- ``NetworkError``: 网络错误
- ``ActionFailed``: API 调用失败
"""
2021-03-31 21:20:07 +08:00
return await super().call_api(api, event=event, **data)
2021-03-31 16:51:09 +08:00
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,
webhook: Optional[str] = None,
secret: Optional[str] = None,
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``: 是否 @ 事件主体
* ``webhook: Optional[str]``: 该条消息将调用的 webhook 地址不传则将使用 sessionWebhook若其也不存在该条消息不发送使用自定义 webhook 时注意你设置的安全方式如加关键词IP地址加签等等
* ``secret: Optional[str]``: 如果你使用自定义的 webhook 地址推荐使用加签方式对消息进行验证 `机器人安全设置页面加签一栏下面显示的SEC开头的字符串` 传入这个参数即可
2020-12-03 00:59:32 +08:00
* ``**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
if webhook:
params["webhook"] = webhook
params["secret"] = secret
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.senderId} " + msg + MessageSegment.atDingtalkIds(
event.senderId)
2020-12-03 00:59:32 +08:00
else:
params["message"] = msg
return await self.call_api(SEND, **params)