mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-07-28 08:41:29 +00:00
✨ ding: send by webhook
close: #189 Feature: 钉钉添加 `send_by_webhook` 方法 #189
This commit is contained in:
@ -1,6 +1,9 @@
|
||||
import hmac
|
||||
import base64
|
||||
import urllib.parse
|
||||
|
||||
from datetime import datetime
|
||||
import time
|
||||
from typing import Any, Union, Optional, TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
@ -10,7 +13,7 @@ from nonebot.message import handle_event
|
||||
from nonebot.adapters import Bot as BaseBot
|
||||
from nonebot.exception import RequestDenied
|
||||
|
||||
from .utils import log
|
||||
from .utils import calc_hmac_base64, log
|
||||
from .config import Config as DingConfig
|
||||
from .message import Message, MessageSegment
|
||||
from .exception import NetworkError, ApiNotAvailable, ActionFailed, SessionExpired
|
||||
@ -20,7 +23,7 @@ if TYPE_CHECKING:
|
||||
from nonebot.config import Config
|
||||
from nonebot.drivers import Driver
|
||||
|
||||
SEND_BY_SESSION_WEBHOOK = "send_by_sessionWebhook"
|
||||
SEND = "send"
|
||||
|
||||
|
||||
class Bot(BaseBot):
|
||||
@ -72,10 +75,8 @@ class Bot(BaseBot):
|
||||
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"):
|
||||
sign_base64 = calc_hmac_base64(str(timestamp), secret)
|
||||
if sign != sign_base64.decode('utf-8'):
|
||||
log("WARNING", "Signature Header is invalid")
|
||||
raise RequestDenied(403, "Signature is invalid")
|
||||
else:
|
||||
@ -142,49 +143,63 @@ class Bot(BaseBot):
|
||||
return await bot.call_api(api, **data)
|
||||
|
||||
log("DEBUG", f"Calling API <y>{api}</y>")
|
||||
params = {}
|
||||
# 传入参数有 webhook,则使用传入的 webhook
|
||||
webhook = data.get("webhook")
|
||||
|
||||
if api == SEND_BY_SESSION_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
|
||||
if event:
|
||||
# 确保 sessionWebhook 没有过期
|
||||
if int(datetime.now().timestamp()) > int(
|
||||
event.sessionWebhookExpiredTime / 1000):
|
||||
raise SessionExpired
|
||||
|
||||
target = event.sessionWebhook
|
||||
webhook = event.sessionWebhook
|
||||
else:
|
||||
raise ApiNotAvailable
|
||||
|
||||
headers = {}
|
||||
message: Message = data.get("message", None)
|
||||
if not message:
|
||||
raise ValueError("Message not found")
|
||||
try:
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
response = await client.post(
|
||||
target,
|
||||
params={"access_token": self.ding_config.access_token},
|
||||
json=message._produce(),
|
||||
timeout=self.config.api_timeout)
|
||||
headers = {}
|
||||
message: Message = data.get("message", None)
|
||||
if not message:
|
||||
raise ValueError("Message not found")
|
||||
try:
|
||||
async with httpx.AsyncClient(headers=headers) 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")
|
||||
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")
|
||||
|
||||
@overrides(BaseBot)
|
||||
async def send(self,
|
||||
event: MessageEvent,
|
||||
message: Union[str, "Message", "MessageSegment"],
|
||||
at_sender: bool = False,
|
||||
webhook: Optional[str] = None,
|
||||
secret: Optional[str] = None,
|
||||
**kwargs) -> Any:
|
||||
"""
|
||||
:说明:
|
||||
@ -196,6 +211,8 @@ class Bot(BaseBot):
|
||||
* ``event: Event``: Event 对象
|
||||
* ``message: Union[str, Message, MessageSegment]``: 要发送的消息
|
||||
* ``at_sender: bool``: 是否 @ 事件主体
|
||||
* ``webhook: Optional[str]``: 该条消息将调用的 webhook 地址。不传则将使用 sessionWebhook,若其也不存在,该条消息不发送,使用自定义 webhook 时注意你设置的安全方式,如加关键词,IP地址,加签等等。
|
||||
* ``secret: Optional[str]``: 如果你使用自定义的 webhook 地址,推荐使用加签方式对消息进行验证,将 `机器人安全设置页面,加签一栏下面显示的SEC开头的字符串` 传入这个参数即可。
|
||||
* ``**kwargs``: 覆盖默认参数
|
||||
|
||||
:返回:
|
||||
@ -213,6 +230,9 @@ class Bot(BaseBot):
|
||||
at_sender = at_sender and bool(event.senderId)
|
||||
params = {}
|
||||
params["event"] = event
|
||||
if webhook:
|
||||
params["webhook"] = webhook
|
||||
params["secret"] = secret
|
||||
params.update(kwargs)
|
||||
|
||||
if at_sender and event.conversationType != ConversationType.private:
|
||||
@ -222,4 +242,4 @@ class Bot(BaseBot):
|
||||
else:
|
||||
params["message"] = msg
|
||||
|
||||
return await self.call_api(SEND_BY_SESSION_WEBHOOK, **params)
|
||||
return await self.call_api(SEND, **params)
|
||||
|
@ -1,3 +1,15 @@
|
||||
import hmac
|
||||
from nonebot.utils import logger_wrapper
|
||||
|
||||
import hashlib
|
||||
import base64
|
||||
log = logger_wrapper("DING")
|
||||
|
||||
|
||||
def calc_hmac_base64(timestamp: str, secret: str):
|
||||
secret_enc = secret.encode('utf-8')
|
||||
string_to_sign = '{}\n{}'.format(timestamp, secret)
|
||||
string_to_sign_enc = string_to_sign.encode('utf-8')
|
||||
hmac_code = hmac.new(secret_enc,
|
||||
string_to_sign_enc,
|
||||
digestmod=hashlib.sha256).digest()
|
||||
return base64.b64encode(hmac_code)
|
||||
|
Reference in New Issue
Block a user