# 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)