2021-03-20 14:49:58 +08:00
|
|
|
|
import json
|
2021-03-11 13:21:18 +08:00
|
|
|
|
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
|
2020-12-03 15:07:03 +08:00
|
|
|
|
|
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
|
2020-12-03 15:07:03 +08:00
|
|
|
|
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
|
|
|
|
|
2021-03-11 13:21:18 +08:00
|
|
|
|
SEND = "send"
|
2020-12-30 18:33:54 +08:00
|
|
|
|
|
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 15:07:03 +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")
|
2021-03-11 13:21:18 +08:00
|
|
|
|
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):
|
2021-08-12 16:45:22 +08:00
|
|
|
|
data: dict = json.loads(message)
|
2021-06-10 21:52:20 +08:00
|
|
|
|
|
|
|
|
|
if not data:
|
2020-12-29 12:12:35 +08:00
|
|
|
|
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:
|
2020-12-30 18:33:54 +08:00
|
|
|
|
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(
|
2021-08-12 16:45:22 +08:00
|
|
|
|
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:
|
2021-09-13 01:18:25 +08:00
|
|
|
|
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>")
|
2021-03-11 13:21:18 +08:00
|
|
|
|
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(
|
2020-12-29 12:12:35 +08:00
|
|
|
|
event.sessionWebhookExpiredTime / 1000):
|
2020-12-03 17:08:16 +08:00
|
|
|
|
raise SessionExpired
|
2020-12-03 00:59:32 +08:00
|
|
|
|
|
2021-03-11 13:21:18 +08:00
|
|
|
|
webhook = event.sessionWebhook
|
2020-12-03 17:08:16 +08:00
|
|
|
|
else:
|
2020-12-03 00:59:32 +08:00
|
|
|
|
raise ApiNotAvailable
|
|
|
|
|
|
2021-03-11 13:21:18 +08:00
|
|
|
|
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:
|
2021-03-11 13:21:18 +08:00
|
|
|
|
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,
|
2021-03-11 13:21:18 +08:00
|
|
|
|
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``: 是否 @ 事件主体
|
2021-03-11 13:21:18 +08:00
|
|
|
|
* ``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
|
2021-03-11 13:21:18 +08:00
|
|
|
|
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:
|
2021-01-29 14:31:36 +08:00
|
|
|
|
params[
|
|
|
|
|
"message"] = f"@{event.senderId} " + msg + MessageSegment.atDingtalkIds(
|
|
|
|
|
event.senderId)
|
2020-12-03 00:59:32 +08:00
|
|
|
|
else:
|
|
|
|
|
params["message"] = msg
|
|
|
|
|
|
2021-03-11 13:21:18 +08:00
|
|
|
|
return await self.call_api(SEND, **params)
|