ad-password-self-service/utils/feishu/api_callback.py

339 lines
14 KiB
Python

# coding: utf-8
from __future__ import absolute_import, division, print_function, unicode_literals
import base64
import hashlib
import json
import logging
from typing import TYPE_CHECKING, Any, Dict
from Crypto.Cipher import AES
from utils.feishu.dt_callback import (EventAppOpen, EventApproval, EventAppTicket, EventContactDepartment, EventContactScope,
EventContactUser, EventLeaveApproval, EventMessage, EventP2PCreateChat,
EventRemedyApproval, EventRemoveAddBot, EventShiftApproval, EventTripApproval,
EventUserInAndOutChat, EventWorkApproval)
from utils.feishu.dt_enum import EventType
from utils.feishu.dt_help import make_datatype
from utils.feishu.exception import LarkInvalidArguments, LarkInvalidCallback
from utils.feishu.helper import pop_or_none
if TYPE_CHECKING:
from utils.feishu.api import OpenLark
logger = logging.getLogger('feishu')
class _AESCipher(object):
def __init__(self, key):
self.bs = AES.block_size
self.key = hashlib.sha256(_AESCipher.str_to_bytes(key)).digest()
@staticmethod
def str_to_bytes(data):
u_type = type(b"".decode('utf8'))
if isinstance(data, u_type):
return data.encode('utf8')
return data # pragma: no cover
@staticmethod
def _unpad(s):
return s[:-ord(s[len(s) - 1:])]
def decrypt(self, enc):
iv = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(enc[AES.block_size:]))
def decrypt_string(self, enc):
enc = base64.b64decode(enc)
return self.decrypt(enc).decode('utf8')
def get_event_type(body):
"""
:param body:
:type body: Dict[string_types, typing.Union[Any, Dict[string_types, Any]]]
:return:
"""
t = body.get('type')
if t == 'event_callback':
t = body.get('event', {}).get('type')
return {
'app_ticket': EventType.app_ticket, # DONE
'app_open': EventType.app_open,
'message': EventType.message, # DONE
'user_add': EventType.user_add,
'user_update': EventType.user_update,
'user_leave': EventType.user_leave,
'dept_add': EventType.dept_add,
'dept_update': EventType.dept_update,
'dept_delete': EventType.dept_delete,
'contact_scope_change': EventType.contact_scope_change,
'approval': EventType.approval,
'leave_approval': EventType.leave_approval,
'work_approval': EventType.work_approval,
'shift_approval': EventType.shift_approval,
'remedy_approval': EventType.remedy_approval,
'trip_approval': EventType.trip_approval,
'remove_bot': EventType.remove_bot,
'add_bot': EventType.add_bot,
'p2p_chat_create': EventType.p2p_chat_create,
}.get(t, EventType.unknown)
return {
'url_verification': EventType.url_verification,
}.get(t, EventType.unknown)
class APICallbackMixin(object):
"""订阅事件
飞书中很多操作都会产生事件,应用可以订阅这些事件来与飞书进行高度整合,可以在开发者后台进行事件订阅配置来监听事件。
已经有的事件类型:
- 审批通过
- 收到消息(必须单聊或者是艾特机器人)
- 推送 app_ticket
https://open.feishu.cn/document/uYjL24iN/uUTNz4SN1MjL1UzM
"""
def handle_callback(
self,
body,
handle_message=None,
handle_app_ticket=None,
handle_approval=None,
handle_leave_approval=None,
handle_work_approval=None,
handle_shift_approval=None,
handle_remedy_approval=None,
handle_trip_approval=None,
handle_app_open=None,
handle_contact_user=None,
handle_contact_department=None,
handle_contact_scope=None,
handle_remove_add_bot=None,
handle_p2p_chat_create=None,
handle_user_in_out_chat=None,
):
"""处理机器人回调
:type self: OpenLark
:param body: 回调的消息主题
:type body: Dict[string_types, Any]
:param handle_message: 消息的回调 - 处理函数
:type handle_message: Callable[[str, str, 'EventMessage', Dict[str, Any]], Any]
:param handle_app_ticket: app_ticket 事件 - 处理函数
:type handle_app_ticket: Callable[[str, str, 'EventAppTicket', Dict[str, Any]], Any]
:param handle_approval:
:type handle_approval: Callable[[str, str, 'EventApproval', Dict[str, Any]], Any]
:param handle_leave_approval:
:type handle_leave_approval: Callable[[str, str, 'EventLeaveApproval', Dict[str, Any]], Any]
:param handle_work_approval:
:type handle_work_approval: Callable[[str, str, 'EventWorkApproval', Dict[str, Any]], Any]
:param handle_shift_approval:
:type handle_shift_approval: Callable[[str, str, 'EventShiftApproval', Dict[str, Any]], Any]
:param handle_remedy_approval:
:type handle_remedy_approval: Callable[[str, str, 'EventRemedyApproval', Dict[str, Any]], Any]
:param handle_trip_approval:
:type handle_trip_approval: Callable[[str, str, 'EventTripApproval', Dict[str, Any]], Any]
:param handle_app_open:
:type handle_app_open: Callable[[str, str, 'EventAppOpen', Dict[str, Any]], Any]
:param handle_contact_user:
:type handle_contact_user: Callable[[str, str, 'EventContactUser', Dict[str, Any]], Any]
:param handle_contact_department:
:type handle_contact_department: Callable[[str, str, 'EventContactDepartment', Dict[str, Any]], Any]
:param handle_contact_scope:
:type handle_contact_scope: Callable[[str, str, 'EventContactScope', Dict[str, Any]], Any]
:param handle_remove_add_bot:
:type handle_remove_add_bot: Callable[[str, str, 'EventRemoveAddBot', Dict[str, Any]], Any]
:param handle_p2p_chat_create:
:type handle_p2p_chat_create: Callable[[str, str, 'EventP2PCreateChat', Dict[str, Any]], Any]
:param handle_user_in_out_chat:
:type handle_user_in_out_chat: Callable[[str, str, 'EventUserInAndOutChat', Dict[str, Any]], Any]
"""
if not isinstance(body, dict):
raise LarkInvalidArguments(msg='回调参数需要是字典')
if 'encrypt' in body:
body = json.loads(self.decrypt_string(body['encrypt']))
if not self.verification_token:
raise LarkInvalidArguments(msg='回调需要 verification_token 参数')
token = body.get('token')
if token != self.verification_token:
raise LarkInvalidCallback(msg='token: {} 不合法'.format(token))
event_type = get_event_type(body)
if event_type == EventType.url_verification:
return {'challenge': body.get('challenge')}
msg_uuid = body.get('uuid', '') # type: str
msg_timestamp = body.get('ts', '') # type: str
json_event = body.get('event', {}) # type: Dict[str, Any]
logger.info('[callback] uuid=%s, ts=%s, event=%s', msg_uuid, msg_timestamp, json_event)
if event_type == EventType.approval:
# 审批通过
if handle_approval:
event_approval = make_datatype(EventApproval, json_event)
return handle_approval(msg_uuid, msg_timestamp, event_approval, json_event)
return
if event_type == EventType.leave_approval:
# 请假审批
if handle_leave_approval:
event_leave_approval = make_datatype(EventLeaveApproval, json_event)
return handle_leave_approval(msg_uuid, msg_timestamp, event_leave_approval, json_event)
return
if event_type == EventType.work_approval:
# 加班审批
if handle_work_approval:
event_work_approval = make_datatype(EventWorkApproval, json_event)
return handle_work_approval(msg_uuid, msg_timestamp, event_work_approval, json_event)
return
if event_type == EventType.shift_approval:
# 换班审批
if handle_shift_approval:
event_shift_approval = make_datatype(EventShiftApproval, json_event)
return handle_shift_approval(msg_uuid, msg_timestamp, event_shift_approval, json_event)
return
if event_type == EventType.remedy_approval:
# 补卡审批
if handle_remedy_approval:
event_remedy_approval = make_datatype(EventRemedyApproval, json_event)
return handle_remedy_approval(msg_uuid, msg_timestamp, event_remedy_approval, json_event)
return
if event_type == EventType.trip_approval:
# 出差审批
if handle_trip_approval:
event_trip_approval = make_datatype(EventTripApproval, json_event)
return handle_trip_approval(msg_uuid, msg_timestamp, event_trip_approval, json_event)
return
if event_type == EventType.app_open:
# 开通应用
if handle_app_open:
event_app_open = make_datatype(EventAppOpen, json_event)
return handle_app_open(msg_uuid, msg_timestamp, event_app_open, json_event)
return
if event_type in [EventType.user_add, EventType.user_leave, EventType.user_update]:
# 通讯录用户相关变更事件,包括 user_add, user_update 和 user_leave 事件类型
if handle_contact_user:
event_contact_user = make_datatype(EventContactUser, json_event)
return handle_contact_user(msg_uuid, msg_timestamp, event_contact_user, json_event)
return
if event_type in [EventType.dept_add, EventType.dept_delete, EventType.dept_update]:
# 通讯录部门相关变更事件,包括 dept_add, dept_update 和 dept_delete
if handle_contact_department:
event_contact_department = make_datatype(EventContactDepartment, json_event)
return handle_contact_department(msg_uuid, msg_timestamp, event_contact_department, json_event)
return
if event_type == EventType.contact_scope_change:
# 变更权限范围
if handle_contact_scope:
event_contact_scope = make_datatype(EventContactScope, json_event)
return handle_contact_scope(msg_uuid, msg_timestamp, event_contact_scope, json_event)
return
if event_type == EventType.message:
# 收到消息(必须单聊或者是艾特机器人)的回调
if handle_message:
event = make_datatype(EventMessage, json_event) # type: EventMessage
return handle_message(msg_uuid, msg_timestamp, event, json_event)
return
if event_type in [EventType.remove_bot, EventType.add_bot]:
# 机器人被移出群聊/机器人被邀请进入群聊
if handle_remove_add_bot:
event_remove_add_bot = make_datatype(EventRemoveAddBot, json_event)
return handle_remove_add_bot(msg_uuid, msg_timestamp, event_remove_add_bot, json_event)
return
if event_type == EventType.app_ticket:
# 下发 app_ticket
event_app_ticket = make_datatype(EventAppTicket, json_event)
self.update_app_ticket(event_app_ticket.app_ticket)
if handle_app_ticket:
return handle_app_ticket(msg_uuid, msg_timestamp, event_app_ticket, json_event)
return
if event_type == EventType.p2p_chat_create:
# 机器人和用户的会话第一次创建
if handle_p2p_chat_create:
event_chat_create = make_datatype(EventP2PCreateChat, json_event)
return handle_p2p_chat_create(msg_uuid, msg_timestamp, event_chat_create, json_event)
return
if event_type in [EventType.add_user_to_chat, EventType.remove_user_from_chat,
EventType.revoke_add_user_from_chat]:
# 用户进群和出群
if handle_user_in_out_chat:
event_in_and_out_chat = make_datatype(EventUserInAndOutChat, json_event)
return handle_user_in_out_chat(msg_uuid, msg_timestamp, event_in_and_out_chat, json_event)
return
logger.warning('[callback][unknown event] uuid=%s, ts=%s, event=%s', msg_uuid, msg_timestamp, event_type)
return {
'message': 'event: {} not handle'.format(event_type),
'msg_uuid': msg_uuid,
'msg_timestamp': msg_timestamp,
'json_event': json_event,
}
def handle_card_message_callback(self, body, handle=None):
"""处理卡片消息的回调
:type self: OpenLark
:type body: Dict[string_types, Any]
:type handle: Callable[[str, str, str, str, str, Dict[str, Any]], Any]
"""
if not isinstance(body, dict):
raise LarkInvalidArguments(msg='回调参数需要是字典')
if 'encrypt' in body:
body = json.loads(self.decrypt_string(body['encrypt']))
event_type = get_event_type(body)
if event_type == EventType.url_verification:
if not self.verification_token:
raise LarkInvalidArguments(msg='回调需要 verification_token 参数')
token = body.get('token')
if token != self.verification_token:
raise LarkInvalidCallback(msg='token: {} 不合法'.format(token))
return {'challenge': body.get('challenge')}
open_id = pop_or_none(body, 'open_id')
employee_id = pop_or_none(body, 'employee_id')
open_message_id = pop_or_none(body, 'open_message_id')
tenant_key = pop_or_none(body, 'tenant_key')
tag = pop_or_none(body, 'tag')
return handle(tenant_key, open_id, employee_id, open_message_id, tag, body)
def decrypt_string(self, s):
"""
:type self: OpenLark
:param s:
:return:
"""
if not self.encrypt_key:
raise LarkInvalidArguments(msg='需要 encrypt_key 参数')
return _AESCipher(self.encrypt_key).decrypt_string(s)