2021-03-20 14:49:58 +08:00
import json
2021-03-11 13:21:18 +08:00
import urllib . parse
2020-12-03 15:07:03 +08:00
from datetime import datetime
2021-03-11 13:21:18 +08:00
import time
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 15:07:03 +08:00
2021-03-11 13:21:18 +08:00
from . utils import calc_hmac_base64 , log
2021-01-17 13:46:29 +08:00
from . config import Config as DingConfig
2020-12-03 15:07:03 +08:00
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
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
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 ,
2021-03-20 14:49:58 +08:00
headers : dict , body : Optional [ bytes ] ) - > str :
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>`_
"""
timestamp = headers . get ( " timestamp " )
sign = headers . get ( " sign " )
# 检查连接方式
if connection_type not in [ " http " ] :
2020-12-29 12:12:35 +08:00
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 " )
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 " )
raise RequestDenied ( 403 , " Signature is invalid " )
else :
log ( " WARNING " , " Ding signature check ignored! " )
2021-03-20 14:49:58 +08:00
return json . loads ( body . decode ( ) ) [ " 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 :
2020-12-29 12:12:35 +08:00
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 :
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 (
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 )
2021-03-31 16:51:09 +08:00
async def _call_api ( self ,
api : str ,
event : Optional [ MessageEvent ] = None ,
* * data ) - > Any :
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
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 :
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 " )
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 调用失败
"""
return super ( ) . call_api ( api , event = event , * * data )
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 )