Esempio 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
Esempio 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)
Esempio n. 3
0
    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)
Esempio n. 4
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)
Esempio n. 5
0
    def invite_user_to_chat(self, chat_id, open_ids=None, user_ids=None):
        """机器人拉用户进群

        :type self: OpenLark
        :param chat_id: 群 ID
        :type chat_id: str
        :param open_ids: 需要加入群的用户的 open_id 列表
        :type open_ids: list[str]
        :param user_ids: 需要加入群的用户的 employee_id 列表
        :type user_ids: list[str]
        :return: invalid_open_ids, invalid_user_ids
        :rtype: Tuple[list[str], list[str]]

        机器人拉用户进群,机器人必须在群里

        https://open.feishu.cn/document/ukTMukTMukTM/uMjMxEjLzITMx4yMyETM
        """
        url = self._gen_request_url('/open-apis/chat/v4/chatter/add/')
        if not open_ids and not user_ids:
            raise LarkInvalidArguments(
                msg='[invite_user_to_chat] empty open_ids and user_ids')
        if not open_ids:
            open_ids = None
        if not user_ids:
            user_ids = None
        body = {'chat_id': chat_id, 'open_ids': open_ids, 'user_ids': user_ids}
        res = self._post(url, body, with_tenant_token=True)
        data = res['data']
        invalid_open_ids = data.get('invalid_open_ids', [])
        invalid_user_ids = data.get('invalid_user_ids', [])
        return invalid_open_ids, invalid_user_ids
Esempio n. 6
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
Esempio n. 7
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'])
Esempio n. 8
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'])
Esempio n. 9
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)
Esempio n. 10
0
    def remove_user_from_chat(self, chat_id, open_ids=None, user_ids=None):
        """机器踢人出群

        :type self: OpenLark
        :param chat_id: 群 ID
        :type chat_id: str
        :param open_ids: 需要踢出群的用户的 open_id 列表
        :type open_ids: list[str]
        :param user_ids: 需要踢出群的用户的 user_ids 列表
        :type user_ids: list[str]
        :return: invalid_open_ids, invalid_employee_ids
        :rtype: Tuple[list[str], list[str]]

        机器人踢用户出群,机器人必须是群主

        https://open.feishu.cn/document/ukTMukTMukTM/uADMwUjLwADM14CMwATN
        """
        if not open_ids and not user_ids:
            raise LarkInvalidArguments(
                msg='[remove_user_from_chat] empty open_ids and user_ids')

        if not open_ids:
            open_ids = None
        if not user_ids:
            user_ids = None
        url = self._gen_request_url('/open-apis/chat/v4/chatter/delete/')
        body = {'chat_id': chat_id, 'open_ids': open_ids, 'user_ids': user_ids}

        res = self._post(url, body, with_tenant_token=True)
        data = res['data']

        invalid_open_ids = data.get('invalid_open_ids', [])  # type: List[str]
        invalid_user_ids = data.get('invalid_user_ids', [])  # type: List[str]

        return invalid_open_ids, invalid_user_ids
Esempio n. 11
0
    def get_admin_scope(self, user_id=None, open_id=None):
        """获取应用管理员管理范围

        :type self: OpenLark
        :param user_id:
        :type user_id: str
        :param open_id:
        :type open_id: str
        :return: is_all, department_ids
            当 is_all 为 true 时,不返回 department_ids
        :rtype: (bool, list[str])

        该接口用于获取应用管理员的管理范围,即该应用管理员能够管理哪些部门。

        https://open.feishu.cn/document/ukTMukTMukTM/uMzN3QjLzczN04yM3cDN
        """

        url = self._gen_request_url(
            '/open-apis/contact/v1/user/admin_scope/get')
        if user_id:
            url = '{}?employee_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')

        res = self._get(url, with_tenant_token=True)
        data = res['data']

        is_all = data.get('is_all')
        department_ids = data.get('department_list', [])
        return is_all, department_ids
Esempio n. 12
0
    def batch_get_department_detail_user(self, user_ids=None, open_ids=None):
        """批量获取用户详细信息

        :type self: OpenLark
        :param user_ids: 用户 UserID 列表
        :type user_ids: list[str]
        :param open_ids: 用户 OpenID 列表
        :type open_ids: list[str]
        :return: has_more, page_token, departments
        :rtype: (Dict[str, DepartmentUser], Dict[str, OpenLarkException])

        批量获取用户信息详情,需具有用户所在部门或者用户的通讯录权限。

        https://open.feishu.cn/document/ukTMukTMukTM/ugjNz4CO2MjL4YzM

        https://bytedance.feishu.cn/docs/doccnOcR1fnxBACchoY9tlg7Amg#
        """
        if user_ids and open_ids:
            raise LarkInvalidArguments(msg='only need user_ids or open_ids')
        elif not user_ids and not open_ids:
            raise LarkInvalidArguments(msg='need user_ids or open_ids')

        qs = ''
        user_key = ''
        if user_ids:
            qs = '&'.join(['user_ids={}'.format(i) for i in user_ids])
            user_key = 'user_id'
        elif open_ids:
            qs = '&'.join(['open_ids={}'.format(i) for i in open_ids])
            user_key = 'open_id'

        url = self._gen_request_url('/open-apis/contact/v2/user/batch_get')
        url = url + '?' + qs
        res = self._get(url, with_tenant_token=True)
        data = res['data']

        users = {}  # type: Dict[str, DepartmentUser]
        for i in data.get('users', []):
            user = make_datatype(DepartmentUser, i)
            users[getattr(user, user_key)] = user

        errors = {}  # type: Dict[str, OpenLarkException]
        for i in data.get('errors', []):
            e = gen_exception(code=i.get('code'), url='', msg=i.get('msg'))
            errors[i.get('id', '')] = e
        return users, errors
Esempio n. 13
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
Esempio n. 14
0
    def create_chat(self,
                    open_ids=None,
                    user_ids=None,
                    name=None,
                    description=None,
                    en_name=None,
                    ja_name=None):
        """机器人创建群并拉用户进群

        :type self: OpenLark
        :param open_ids: 成员 open_id 列表
        :type open_ids: list[str]
        :param user_ids: 成员 user_id 列表
        :type user_ids: list[str]
        :param name: 群的名称
        :type name: str
        :param description: 群描述
        :type description: str
        :param en_name: 群的英文名称
        :type en_name: str
        :param ja_name: 群的日文名称
        :type ja_name: str
        :return: open_chat_id, invalid_open_ids, invalid_user_ids
        :rtype: Tuple[str, list[str], list[str]]

        https://open.feishu.cn/document/ukTMukTMukTM/ukDO5QjL5gTO04SO4kDN
        """
        if not open_ids and not user_ids:
            raise LarkInvalidArguments(msg='open_ids or user_ids cannot empty')

        url = self._gen_request_url('/open-apis/chat/v4/create/')
        body = {}  # type: Dict[string_types, Any]
        if open_ids:
            body['open_ids'] = open_ids
        if user_ids:
            body['user_ids'] = user_ids
        if name:
            body['name'] = name
        if description:
            body['description'] = description
        if name is not None:
            body['name'] = name
        if description is not None:
            body['description'] = description
        if en_name or ja_name:
            body['i18n_names'] = {
                "zh_cn": name,
                "en_us": en_name,
                'ja_jp': ja_name,
            }

        res = self._post(url, body, with_tenant_token=True)
        data = res['data']
        open_chat_id = data.get('chat_id', '')  # type: str
        invalid_open_ids = data.get('invalid_open_ids', [])  # type: List[str]
        invalid_user_ids = data.get('invalid_user_ids', [])  # type: List[str]

        return open_chat_id, invalid_open_ids, invalid_user_ids
Esempio n. 15
0
    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)
Esempio n. 16
0
def converter_enum(value, ranges=None):
    v = value.value if isinstance(value, Enum) else value

    if ranges is not None:
        ranges_v = [i.value if isinstance(i, Enum) else i for i in ranges]
        if v not in ranges_v:
            raise LarkInvalidArguments(msg='enum: %s should be in ranges: %s' %
                                       (v, ' / '.join(map(str, ranges_v))))

    return v
Esempio n. 17
0
    def create_approval(self,
                        definition_code,
                        employee_id,
                        department_id,
                        form_list,
                        approver_employee_id_list,
                        cc_employee_id_list=None,
                        node_approver_employee_id_list=None):
        """创建审批实例

        :type self: OpenLark
        :param definition_code: 审批定义 code,需要有管理员权限,然后在 https://www.feishu.cn/approval/admin/approvalList 创建
        :type definition_code: str
        :param employee_id: 租户内用户唯一 ID
        :type employee_id: str
        :param department_id: 部门 ID
        :type  department_id: str
        :param form_list: 审批的表单内容
        :type form_list: list[(str, Any)]
        :param approver_employee_id_list: 审批人用户 ID 列表
        :type approver_employee_id_list: list[str]
        :param cc_employee_id_list: 抄送人用户 ID 列表
        :type cc_employee_id_list: list[str]
        :param node_approver_employee_id_list: 发起人自选审批人列表
        :type node_approver_employee_id_list: Dict[str, list[str]]
        :return: 审批实例的 instance_code
        :rtype: str

        创建一个审批实例,调用方需对审批定义的表单有详细了解,将按照定义的表单结构,将表单 Value 通过接口传入。

        https://open.feishu.cn/document/ukTMukTMukTM/uYDO24iN4YjL2gjN
        """
        form = []
        for i in form_list:
            if len(i) != 2:
                raise LarkInvalidArguments(msg='the length of item in a form_list be 2(key and value)')
            form.append({'id': i[0], 'value': i[1]})

        url = self._gen_request_url('/approval/openapi/v1/instance/create', app='approval')
        if not cc_employee_id_list:
            cc_employee_id_list = []
        if not node_approver_employee_id_list:
            node_approver_employee_id_list = {}
        body = {
            'definition_code': definition_code,
            'employee_id': employee_id,
            'department_id': department_id,
            'form': json.dumps(form),
            'approver_employee_id_list': approver_employee_id_list,
            'cc_employee_id_list': cc_employee_id_list,
            'node_approver_employee_id_list': node_approver_employee_id_list,
        }
        res = self._post(url, body=body, with_tenant_token=True)
        return res.get('data', {}).get('instance_code', '')
Esempio n. 18
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
Esempio n. 19
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
Esempio n. 20
0
    def tenant_access_token(self):
        """获取 tenant_access_token

        :rtype str

        注意:如果是 ISV 应用,那么必须在构造 OpenLark 实例的时候,必须传入 is_isv=True 和 tenant_key

        https://open.feishu.cn/document/ukTMukTMukTM/uIjNz4iM2MjLyYzM
        https://open.feishu.cn/document/ukTMukTMukTM/uMjNz4yM2MjLzYzM
        """

        key_token = 'open_lark: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] '
                    'isv 应用获取 tenant_access_token 必须先设置 tenant_key')

            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
Esempio n. 21
0
    def send_post(self,
                  zh_cn_title,
                  zh_cn_content,
                  en_us_title=None,
                  en_us_content=None):
        """发送富文本消息

        :param zh_cn_title: 中文标题
        :type zh_cn_title: str
        :param zh_cn_content: 中文内容,是 MessageText, MessageAt, MessageImage, MessageLink 的二维数组
        :type zh_cn_content: List[List[Union[MessageText, MessageAt, MessageImage, MessageLink]]]
        :param en_us_title: 英文标题
        :type en_us_title: str
        :param en_us_content: 英文内容,是 MessageText, MessageAt, MessageImage, MessageLink 的二维数组
        :type en_us_content: List[List[Union[MessageText, MessageAt, MessageImage, MessageLink]]]
        :return: 发送成功的消息 open_message_id
        """
        if not zh_cn_title and not en_us_title and not zh_cn_content and not en_us_content:
            raise LarkInvalidArguments(msg='send post message with empty content')

        body = deepcopy(self.__to)
        if self.__root_id:
            body['root_id'] = self.__root_id
        body['msg_type'] = MessageType.post
        body['content'] = {
            'post': {
                'zh_cn': {
                    'title': zh_cn_title,
                    'content': [list(map(lambda cls: cls.as_post_dict(), i)) for i in zh_cn_content],
                }
            }
        }
        if en_us_title is not None or en_us_content is not None:
            body['content']['post']['en_us'] = {}
            if en_us_title is not None:
                body['content']['post']['en_us']['title'] = en_us_title
            if en_us_content is not None:
                body['content']['post']['en_us']['content'] = \
                    [list(map(lambda cls: cls.as_post_dict(), i)) for i in en_us_content]

        return _send_all_message(self.__open_lark, **body)
Esempio n. 22
0
    def get_chat_id_between_users(self, to_user_id, open_id='', user_id=''):
        """获取用户和用户的之前的 chat_id

        :type self: OpenLark
        :param to_user_id: 到谁的 open_id
        :type to_user_id: str
        :param open_id: 从谁来的 open_id
        :type open_id: str
        :param user_id: 从谁来的 user_id
        :type user_id: str
        :return: 两个人之间的 open_chat_id, chat_id
        :rtype: Tuple[str, str]

        仅头条内部用户可用 需要申请权限才能获取 @fanlv

        open_id 和 user_id 传一个就行

        https://lark-open.bytedance.net/document/ukTMukTMukTM/uYjMxEjL2ITMx4iNyETM

        """
        if open_id:
            url = self._gen_request_url(
                '/open-apis/chat/v3/p2p/id?open_id={}&chatter={}'.format(
                    open_id, to_user_id))
        elif user_id:
            url = self._gen_request_url(
                '/open-apis/chat/v3/p2p/id?user_id={}&chatter={}'.format(
                    user_id, to_user_id))
        else:
            raise LarkInvalidArguments(
                msg='[get_chat_id_between_users] empty open_id and user_id')

        res = self._get(url, with_tenant_token=True)
        open_chat_id = res.get('open_chat_id', '')  # type: str
        chat_id = res.get('chat_id', '')  # type: str
        return open_chat_id, chat_id
Esempio n. 23
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 = 'open_lark:{}: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
Esempio n. 24
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,
        }
Esempio n. 25
0
    def delete_user(self,
                    user_user_id=None,
                    user_open_id=None,
                    department_chat_acceptor_user_id=None,
                    department_chat_acceptor_open_id=None,
                    external_chat_acceptor_user_id=None,
                    external_chat_acceptor_open_id=None,
                    docs_acceptor_user_id=None,
                    docs_acceptor_open_id=None,
                    calendar_acceptor_user_id=None,
                    calendar_acceptor_open_id=None,
                    application_acceptor_user_id=None,
                    application_acceptor_open_id=None):
        """删除用户

        :type self: OpenLark
        :param user_user_id: 被删除用户,请求至少包含被删除用户的 user_id 或者 open_id 之一,同时传递两个参数时按 user_id 处理
        :type user_user_id: str
        :param user_open_id: 被删除用户,请求至少包含被删除用户的 user_id 或者 open_id 之一,同时传递两个参数时按 user_id 处理
        :type user_open_id: str
        :param department_chat_acceptor_user_id: 部门群接收者,
            被删除用户为部门群群主时,转让群主给指定接收者,不指定接收者则默认转让给群内第一个入群的人
        :type department_chat_acceptor_user_id: str
        :param department_chat_acceptor_open_id: 部门群接收者,
            被删除用户为部门群群主时,转让群主给指定接收者,不指定接收者则默认转让给群内第一个入群的人
        :type department_chat_acceptor_open_id: str
        :param external_chat_acceptor_user_id: 外部群接收者,
            被删除用户为外部群群主时,转让群主给指定接收者,不指定接收者则默认转让给群内与被删除用户在同一组织的第一个入群的人,
            如果组织内只有该用户在群里,则解散外部群
        :type external_chat_acceptor_user_id: str
        :param external_chat_acceptor_user_id: 外部群接收者,
            被删除用户为外部群群主时,转让群主给指定接收者,不指定接收者则默认转让给群内与被删除用户在同一组织的第一个入群的人,
            如果组织内只有该用户在群里,则解散外部群
        :type external_chat_acceptor_user_id: str
        :param external_chat_acceptor_open_id: 文档接收者
            用户被删除时,其拥有的文档转让给接收者,不指定接收者则默认转让给直接领导,如果无直接领导则直接删除文档资源
        :type external_chat_acceptor_open_id: str
        :param docs_acceptor_user_id: 文档接收者
            用户被删除时,其拥有的文档转让给接收者,不指定接收者则默认转让给直接领导,如果无直接领导则直接删除文档资源
        :type docs_acceptor_user_id: str
        :param docs_acceptor_open_id: 文档接收者
            用户被删除时,其拥有的文档转让给接收者,不指定接收者则默认转让给直接领导,如果无直接领导则直接删除文档资源
        :type docs_acceptor_open_id: str
        :param calendar_acceptor_user_id: 日程接收者
            用户被删除时,其拥有的日程转让给接收者,不指定接收者则默认转让给直接领导,如果无直接领导则直接删除日程资源
        :type calendar_acceptor_user_id: str
        :param calendar_acceptor_open_id: 日程接收者
            用户被删除时,其拥有的日程转让给接收者,不指定接收者则默认转让给直接领导,如果无直接领导则直接删除日程资源
        :type calendar_acceptor_open_id: str
        :param application_acceptor_user_id: 应用接收者
            用户被删除时,其创建的应用转让给接收者,不指定接收者则默认转让给直接领导,如果无直接领导则不会转移应用,会造成应用不可用
        :type application_acceptor_user_id: str
        :param application_acceptor_open_id: 应用接收者
            用户被删除时,其创建的应用转让给接收者,不指定接收者则默认转让给直接领导,如果无直接领导则不会转移应用,会造成应用不可用
        :type application_acceptor_open_id: str

        该接口用于从通讯录中删除用户。

        调用该接口需要具有该用户或者用户所在部门的通讯录权限。

        应用商店应用无权限调用接口。

        https://open.feishu.cn/document/ukTMukTMukTM/uUzNz4SN3MjL1czM
        """
        if not user_user_id and not user_open_id:
            raise LarkInvalidArguments(msg='empty user user_id and open_id')

        url = self._gen_request_url('/open-apis/contact/v1/user/delete')
        body = {
            'employee_id': user_user_id,
            'open_id': user_open_id,
            'department_chat_acceptor': {
                'employee_id': department_chat_acceptor_user_id,
                'open_id': department_chat_acceptor_open_id,
            },
            'external_chat_acceptor': {
                'employee_id': external_chat_acceptor_user_id,
                'open_id': external_chat_acceptor_open_id,
            },
            'docs_acceptor': {
                'employee_id': docs_acceptor_user_id,
                'open_id': docs_acceptor_open_id,
            },
            'calendar_acceptor': {
                'employee_id': calendar_acceptor_user_id,
                'open_id': calendar_acceptor_open_id,
            },
            'application_acceptor': {
                'employee_id': application_acceptor_user_id,
                'open_id': application_acceptor_open_id,
            },
        }

        self._post(url, body=body, with_tenant_token=True)