Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
0
    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)
Ejemplo n.º 4
0
    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('<', '&lt;'). \
            replace('>', '&gt;'). \
            replace('&', '&amp;'). \
            replace('\'', '&#x27;'). \
            replace('"', '&quot;')

        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'])
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
    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'])
Ejemplo n.º 7
0
    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
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
    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
Ejemplo n.º 12
0
    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,
        }
Ejemplo n.º 13
0
    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