def as_post_dict(self): i18n_text = self.zh_cn or self.en_us or self.ja_jp if i18n_text: logging.warning('zh_cn or en_us or ja_jp is for card text') multi_platform_href = self.pc_href or self.ios_href or self.android_href if multi_platform_href: logging.warning( 'pc_href or ios_href or android_href is for card text') text = self.text or i18n_text if text is None: raise LarkInvalidArguments(msg='[message] empty text') href = self.href or multi_platform_href if href is None: raise LarkInvalidArguments(msg='[message] empty href') d = { 'tag': 'a', 'text': text, 'href': href, } # type: Dict[string_types, Any] if self.un_escape is not None: d['un_escape'] = self.un_escape return d
def app_ticket(self): """获取 app_ticket """ if not self.is_isv: raise LarkInvalidArguments( msg='[app_ticket] 非 isv 应用无法调用 app_ticket') res = self.__token_getter(self.__key_app_ticket) if not res: logger.warning('[app_ticket] no found, try resend.') self.resend_app_ticket() for t in range(3): sleep_time = 2**t logger.warning( '[app_ticket] had resend, wait to got app_ticket, time=%d, sleep=%d', t + 1, sleep_time) time.sleep(sleep_time) res = self.__token_getter(self.__key_app_ticket) if res: logger.warning( '[app_ticket] resend to got app_ticket not found, time=%d', t + 1) break if not res: raise LarkGetAppTicketFail() if not isinstance(res, string_types): raise LarkInvalidArguments( msg='response of token_getter must be str or bytes, but got {}' .format(type(res))) return to_native(res)
def update_app_visibility(self, app_id, add_users=None, del_users=None, add_departments=None, del_departments=None, is_visiable_to_all=None): """更新应用可用范围 :type self: OpenLark :param app_id: 应用 ID :type app_id: str :param add_users: 增加的用户列表,元素个数不超过500,先增加后删除 :type add_users: list[SimpleUser] :param del_users: 删除的用户列表,元素个数不超过 500,先增加后删除 :type del_users: list[SimpleUser] :param add_departments: 添加的部门列表,元素个数不超过 500,先增加后删除 :type add_departments: list[str] :param del_departments: 删除的部门列表,元素个数不超过 500,先增加后删除 :type del_departments: list[str] :param is_visiable_to_all: 是否全员可见,不填:继续当前状态不改变 :type is_visiable_to_all: bool 该接口用于增加或者删除指定应用被哪些人可用,只能被企业自建应用调用且需要“管理应用”权限。 https://open.feishu.cn/document/ukTMukTMukTM/ucDN3UjL3QzN14yN0cTN """ url = self._gen_request_url('/open-apis/application/v3/app/update_visibility') _add_users = [] for i in (add_users or []): if i.user_id: _add_users.append({'user_id': i.user_id}) elif i.open_id: _add_users.append({'open_id': i.open_id}) else: raise LarkInvalidArguments(msg='empty user_id and open_id') _del_users = [] for i in (del_users or []): if i.user_id: _del_users.append({'user_id': i.user_id}) elif i.open_id: _del_users.append({'open_id': i.open_id}) else: raise LarkInvalidArguments(msg='empty user_id and open_id') body = { 'app_id': app_id, 'add_users': _add_users, 'del_users': _del_users, 'add_departments': add_departments or [], 'del_departments': del_departments or [] } if is_visiable_to_all is not None: body['is_visiable_to_all'] = int(is_visiable_to_all) self._post(url, body=body, with_tenant_token=True)
def add_drive_comment(self, user_access_token, file_token, content): """添加全文评论 :type self: OpenLark :param user_access_token: user_access_token :type user_access_token: str :param file_token: 文件的 token :type file_token: str :param content: 评论内容 :type content: str :return: 评论对象 :rtype: DriveComment 该接口用于根据 file_token 给文档添加全文评论 https://open.feishu.cn/document/ukTMukTMukTM/ucDN4UjL3QDO14yN0gTN """ if not content or not isinstance(content, string_types): raise LarkInvalidArguments(msg='content empty') content = content. \ replace('<', '<'). \ replace('>', '>'). \ replace('&', '&'). \ replace('\'', '''). \ replace('"', '"') url = self._gen_request_url('/open-apis/comment/add_whole') body = {'type': 'doc', 'token': file_token, 'content': content} res = self._post(url, body=body, auth_token=user_access_token) return make_datatype(DriveComment, res['data'])
def get_user(self, open_id='', user_id=''): """获取用户信息 :type self: OpenLark :param open_id: 用户的 open_id :type open_id: str :param user_id: 用户的 user_id :type user_id: str :return: User 对象 :rtype: User https://open.feishu.cn/document/ukTMukTMukTM/ukjMwUjL5IDM14SOyATN """ if open_id: url = self._gen_request_url( '/open-apis/user/v3/info?open_id={}'.format(open_id)) elif user_id: url = self._gen_request_url( '/open-apis/user/v3/info?employee_id={}'.format(user_id)) else: raise LarkInvalidArguments( msg='[get_user] empty open_id and user_id') res = self._get(url, with_tenant_token=True) res['user_id'] = pop_or_none(res, 'employee_id') return make_datatype(User, res)
def delete_drive_file(self, user_access_token, file_token, file_type): """删除云空间文件 :type self: OpenLark :param user_access_token: user_access_token :type user_access_token: str :param file_token: 文件的 token :type file_token: str :param file_type: 文档类型,可选值为 doc 和 sheet :type file_type: DriveFileType :return: 文件夹元信息 :rtype: DriveDeleteFile 本文档包含两个接口,分别用于删除 Doc 和 Sheet,对应的文档类型请调用对应的接口 文档只能被文档所有者删除,文档被删除后将会放到回收站里 https://open.feishu.cn/document/ukTMukTMukTM/uATM2UjLwEjN14CMxYTN """ if converter_enum(file_type) == 'doc': url = self._gen_request_url( '/open-apis/drive/explorer/v2/file/docs/{}'.format(file_token)) elif converter_enum(file_type) == 'sheet': url = self._gen_request_url( '/open-apis/drive/explorer/v2/file/spreadsheets/{}'.format( file_token)) else: raise LarkInvalidArguments( msg='delete file type should be doc or sheet') res = self._delete(url, auth_token=user_access_token) return make_datatype(DriveDeleteFile, res['data'])
def is_user_admin(self, open_id=None, employee_id=None): """获取应用管理权限 :type self: OpenLark :param open_id: 用户 open_id :type open_id: str :param employee_id: 用户租户 ID :type employee_id: str :return: 是否是应用管理员 :rtype: bool 该接口用于查询用户是否为应用管理员。 https://open.feishu.cn/document/ukTMukTMukTM/uITN1EjLyUTNx4iM1UTM """ if open_id: url = '/open-apis/application/v3/is_user_admin?open_id={}'.format(open_id) elif employee_id: url = '/open-apis/application/v3/is_user_admin?employee_id={}'.format(employee_id) else: raise LarkInvalidArguments(msg='[is_user_admin] empty open_id and employee_id') url = self._gen_request_url(url) res = self._get(url, with_tenant_token=True) data = res['data'] return data['is_app_admin'] # type: bool
def join_range(sheet_id, range): if not range or not isinstance(range, string_types): raise LarkInvalidArguments(msg='empty range') for i in [sheet_id, sheet_id + '!']: if range.startswith(i): range = range[len(i):] return sheet_id + '!' + range
def get_visible_apps(self, user_id=None, open_id=None, page_size=20, page_token='', lang=I18NType.zh_cn): """获取应用在企业内的可用范围 :type self: OpenLark :param user_id: 目标用户 user_id,与 open_id 至少给其中之一,user_id 优先于 open_id :type user_id: str :param open_id: 目标用户 open_id :type open_id: str :param page_size: 本次拉取用户列表最大个数(最大值 1000 ,0 自动最大个数 ) :type page_size: int :param page_token: 分页拉取用户列表起始位置标示,不填表示从头开始 :type page_token: str :param lang: 优先展示的应用信息的语言版本(zh_cn:中文,en_us:英文,ja_jp:日文) :type lang: I18NType :return: 是否还有更多, page_token, page_size, 总数, 语言, 应用列表 :rtype: (bool, str, int, int, I18NType, list[App]) 该接口用于查询应用在该企业内可以被使用的范围,只能被企业自建应用调用且需要“获取应用信息”权限。 https://open.feishu.cn/document/ukTMukTMukTM/uIjM3UjLyIzN14iMycTN """ url = self._gen_request_url('/open-apis/application/v1/user/visible_apps?') if user_id: url = '{}&user_id={}'.format(url, user_id) elif open_id: url = '{}&open_id={}'.format(url, open_id) else: raise LarkInvalidArguments(msg='empty user_id and open_id') if page_token: url = '{}&page_token={}'.format(url, page_token) if page_size: url = '{}&page_size={}'.format(url, page_size) if lang: url = '{}&lang={}'.format(url, converter_enum(lang)) res = self._get(url, with_tenant_token=True) data = res['data'] apps = [make_datatype(App, i) for i in data.get('app_list', [])] # type: List[App] has_more = bool(data.get('has_more', False)) # type: bool lang = I18NType(data.get('lang', 'zh_cn')) # type: I18NType page_size = data.get('page_size') # type: int page_token = data.get('page_token') # type: str total_count = data.get('total_count') # type: int return has_more, page_token, page_size, total_count, lang, apps
def as_dict(self): d = {} if self.email is not None: d['member_id'] = self.email d['member_type'] = 'email' elif self.open_id is not None: d['member_type'] = 'openid' d['member_id'] = self.open_id elif self.chat_id is not None: d['member_type'] = 'openchat' d['member_id'] = self.chat_id elif self.employee_id is not None: d['member_type'] = 'userid' d['member_id'] = self.employee_id else: raise LarkInvalidArguments(msg='email / open_id / chat_id / uid 必须有一个') return d
def tenant_access_token(self): """获取 tenant_access_token :rtype str 注意:如果是 ISV 应用,那么必须在构造 OpenLark 实例的时候,必须传入 is_isv=True 和 tenant_key https://open.feishu.cn/document/ukTMukTMukTM/uMjNz4yM2MjLzYzM """ key_token = 'feishu:tenant_token:{}:{}'.format(self.app_id, self.tenant_key) cache_token = self.__token_getter(key_token) if cache_token: return to_native(cache_token) if self.is_isv: if not self.tenant_key: raise LarkInvalidArguments( msg='[tenant_access_token] ' 'must set tenant_key for isv app get tenant_access_token') body = { 'app_access_token': self.app_access_token, 'tenant_key': self.tenant_key, } url = self._gen_request_url( '/open-apis/auth/v3/tenant_access_token/') else: body = {'app_id': self.app_id, 'app_secret': self.app_secret} url = self._gen_request_url( '/open-apis/auth/v3/tenant_access_token/internal/') res = self._post(url, body) tenant_access_token = res['tenant_access_token'] expire = res['expire'] if expire <= 360: return tenant_access_token self.__token_setter(key_token, tenant_access_token, expire - 100) return tenant_access_token
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 __init__(self, app_id, app_secret, encrypt_key=None, verification_token='', oauth_redirect_uri='', token_setter=None, token_getter=None, is_isv=False, is_lark=False, tenant_key='', is_staging=False, ignore_ssl=False): """构造 OpenLark :param app_id: 应用唯一的 ID 标识 :type app_id: string_types :param app_secret: 应用的秘钥,创建 App 的时候由平台生成 :type app_secret: string_types :param encrypt_key: 应用的 AppID, :type encrypt_key: string_types :param verification_token: 用于验证回调是否是开放平台发送的 :type verification_token: string_types :param oauth_redirect_uri: 用于 OAuth 登录的重定向地址 :type oauth_redirect_uri: string_types :param token_setter: 用于分布式设置 token :type token_setter: Callable[[str, str, int], Any] :param token_getter: 用于分布式获取 token :type token_getter: Callable[[str], Optional[Union[str, bytes]]] :param is_isv: 指定本实例是否是 ISV 应用,在获取 tenant_access_token 的时候会使用不同的参数 :type is_isv: bool :param tenant_key: 租户的唯一 ID,如果实例是 ISV 应用,必须指定本参数,在获取 tenant_access_token 的时候会使用不同的参数 :type tenant_key: string_types :param is_staging: 是否是 staging 环境 :param ignore_ssl: 忽略 ssl """ self.app_id = app_id self.app_secret = app_secret self.oauth_redirect_uri = oauth_redirect_uri self.encrypt_key = encrypt_key # 解密回调数据 self.verification_token = verification_token # 回调的时候会有这个字段,校验一致性 self.is_isv = is_isv # 是否是 ISV 应用 self.is_lark = is_lark # 是 Lark 还是飞书 self.tenant_key = tenant_key # 租户 key self.is_staging = is_staging self.ignore_ssl = ignore_ssl self.__key_app_ticket = 'feishu:{}:app_ticket'.format(app_id) if is_isv and not tenant_key: # 在一开始的时候,是不知道 tenant_key 是多少的,又依赖本库解析参数,所以可以先不设置 logger.warning('[init_class] 设置 is_isv 的时候,没有设置 tenant_key') # 数据是一份的,所以必须同时有或者没有 if (token_getter and not token_setter) or (not token_getter and token_setter): raise LarkInvalidArguments( msg='token_getter / token_setter 必须同时设置或者不设置') if not token_setter: token_getter, token_setter = _gen_default_token_getter_setter() self.__token_getter = token_getter self.__token_setter = token_setter