mirror of
				https://github.com/nonebot/nonebot2.git
				synced 2025-10-31 15:06:42 +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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user