├── .gitignore ├── LICENSE ├── README.md ├── client.py ├── composite_api ├── base │ └── create_app_and_tables.py ├── contact │ └── list_user_by_department.py ├── im │ ├── send_file.py │ └── send_image.py └── sheets │ ├── copy_and_paste_by_range.py │ └── download_media_by_range.py ├── config.py ├── quick_start └── robot │ ├── alert.png │ ├── im.py │ ├── im_test.py │ └── main.py ├── requirements.txt └── tests ├── base └── create_app_and_tables_test.py ├── contact └── list_user_by_department_test.py ├── im ├── send_file_test.py └── send_image_test.py └── sheets ├── copy_and_paste_by_range_test.py └── download_media_by_range_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | */__pycache__/ 2 | *.pyc 3 | .DS_Store 4 | .idea/ 5 | quick_start/robot/chat_history.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lark Technologies Pte. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 飞书开放接口使用示例 2 | * 针对多 API 串行调用场景,封装 API [组合函数](./composite_api),减少开发者对接 API 个数,提高开发效率; 3 | * 针对常见业务场景,封装可直接运行的 [Quick-Start](./quick_start),帮助开发者快速上手 API 接入。 4 | 5 | ## 组合函数 6 | 目前提供以下组合函数: 7 | * 消息 8 | * [发送文件消息](./composite_api/im/send_file.py) 9 | * [发送图片消息](./composite_api/im/send_image.py) 10 | * 通讯录 11 | * [获取部门下所有用户列表](./composite_api/contact/list_user_by_department.py) 12 | * 多维表格 13 | * [创建多维表格同时添加数据表](./composite_api/base/create_app_and_tables.py) 14 | * 电子表格 15 | * [复制粘贴某个范围的单元格数据](./composite_api/sheets/copy_and_paste_by_range.py) 16 | * [下载指定范围单元格的所有素材列表](./composite_api/sheets/download_media_by_range.py) 17 | 18 | 19 | ## Quick-Start 20 | 目前提供以下场景的运行示例: 21 | * [机器人自动拉群报警](./quick_start/robot) ([开发教程](https://open.feishu.cn/document/home/message-development-tutorial/introduction)) 22 | 23 | 24 | ## License 25 | MIT 26 | 27 | ## 加入讨论群 28 | [_单击_](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=575k28fa-2c12-400a-80c0-2d8924e00d38)加入讨论群 -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | import lark_oapi as lark 2 | 3 | from config import * 4 | 5 | client = lark.Client.builder() \ 6 | .app_id(APP_ID) \ 7 | .app_secret(APP_SECRET) \ 8 | .log_level(lark.LogLevel.DEBUG) \ 9 | .build() 10 | -------------------------------------------------------------------------------- /composite_api/base/create_app_and_tables.py: -------------------------------------------------------------------------------- 1 | """ 2 | 创建多维表格同时添加数据表,使用到两个OpenAPI: 3 | 1. [创建多维表格](https://open.feishu.cn/document/server-docs/docs/bitable-v1/app/create) 4 | 2. [新增一个数据表](https://open.feishu.cn/document/server-docs/docs/bitable-v1/app-table/create) 5 | """ 6 | 7 | import lark_oapi as lark 8 | from lark_oapi.api.bitable.v1 import * 9 | 10 | 11 | class CreateAppAndTablesRequest(object): 12 | def __init__(self) -> None: 13 | super().__init__() 14 | self.name: Optional[str] = None # 多维表格名称,必填 15 | self.folder_token: Optional[str] = None # 多维表格归属文件夹,必填 16 | self.tables: List[ReqTable] = [] # 数据表,不填则不创建 17 | 18 | 19 | class CreateAppAndTablesResponse(BaseResponse): 20 | def __init__(self): 21 | super().__init__() 22 | self.create_app_response: Optional[CreateAppResponseBody] = None 23 | self.create_app_tables_response: List[CreateAppTableResponseBody] = [] 24 | 25 | 26 | # 创建多维表格同时添加数据表 27 | def create_app_and_tables(client: lark.Client, request: CreateAppAndTablesRequest) -> BaseResponse: 28 | # 创建多维表格 29 | create_app_req = CreateAppRequest.builder() \ 30 | .request_body(ReqApp.builder() 31 | .name(request.name) 32 | .folder_token(request.folder_token) 33 | .build()) \ 34 | .build() 35 | 36 | create_app_resp = client.bitable.v1.app.create(create_app_req) 37 | 38 | if not create_app_resp.success(): 39 | lark.logger.error( 40 | f"client.bitable.v1.app.create failed, " 41 | f"code: {create_app_resp.code}, " 42 | f"msg: {create_app_resp.msg}, " 43 | f"log_id: {create_app_resp.get_log_id()}") 44 | return create_app_resp 45 | 46 | # 添加数据表 47 | option = lark.RequestOption.builder().headers({"X-Tt-Logid": create_app_resp.get_log_id()}).build() 48 | tables = [] 49 | for table in request.tables: 50 | create_app_table_req = CreateAppTableRequest.builder() \ 51 | .app_token(create_app_resp.data.app.app_token) \ 52 | .request_body(CreateAppTableRequestBody.builder() 53 | .table(table) 54 | .build()) \ 55 | .build() 56 | 57 | create_app_table_resp = client.bitable.v1.app_table.create(create_app_table_req, option) 58 | 59 | if not create_app_table_resp.success(): 60 | lark.logger.error( 61 | f"client.bitable.v1.app_table.create failed, " 62 | f"code: {create_app_table_resp.code}, " 63 | f"msg: {create_app_table_resp.msg}, " 64 | f"log_id: {create_app_table_resp.get_log_id()}") 65 | return create_app_table_resp 66 | 67 | tables.append(create_app_table_resp.data) 68 | 69 | # 返回结果 70 | response = CreateAppAndTablesResponse() 71 | response.code = 0 72 | response.msg = "success" 73 | response.create_app_response = create_app_resp.data 74 | response.create_app_tables_response = tables 75 | 76 | return response 77 | -------------------------------------------------------------------------------- /composite_api/contact/list_user_by_department.py: -------------------------------------------------------------------------------- 1 | """ 2 | 获取部门下所有用户列表,使用到两个OpenAPI: 3 | 1. [获取子部门列表](https://open.feishu.cn/document/server-docs/contact-v3/department/children) 4 | 2. [获取部门直属用户列表](https://open.feishu.cn/document/server-docs/contact-v3/user/find_by_department) 5 | """ 6 | 7 | import lark_oapi as lark 8 | from lark_oapi.api.contact.v3 import * 9 | 10 | 11 | class ListUserByDepartmentRequest(object): 12 | def __init__(self) -> None: 13 | super().__init__() 14 | self.department_id: Optional[str] = None # open_department_id,必填 15 | 16 | 17 | class ListUserByDepartmentResponse(BaseResponse): 18 | def __init__(self): 19 | super().__init__() 20 | self.children_department_response: Optional[ChildrenDepartmentResponseBody] = None 21 | self.find_by_department_user_response: List[User] = [] 22 | 23 | 24 | # 获取部门下所有用户列表 25 | def list_user_by_department(client: lark.Client, request: ListUserByDepartmentRequest) -> BaseResponse: 26 | # 获取子部门列表 27 | children_department_req = ChildrenDepartmentRequest.builder() \ 28 | .department_id_type("open_department_id") \ 29 | .department_id(request.department_id) \ 30 | .fetch_child(True) \ 31 | .build() 32 | 33 | children_department_resp = client.contact.v3.department.children(children_department_req) 34 | 35 | if not children_department_resp.success(): 36 | lark.logger.error( 37 | f"client.contact.v3.department.children failed, " 38 | f"code: {children_department_resp.code}, " 39 | f"msg: {children_department_resp.msg}, " 40 | f"log_id: {children_department_resp.get_log_id()}") 41 | return children_department_resp 42 | 43 | # 获取部门直属用户列表 44 | users = [] 45 | open_department_ids = [request.department_id] 46 | for i in children_department_resp.data.items: 47 | open_department_ids.append(i.open_department_id) 48 | 49 | for id in open_department_ids: 50 | find_by_department_user_req = FindByDepartmentUserRequest.builder() \ 51 | .department_id(id) \ 52 | .build() 53 | 54 | find_by_department_user_resp = client.contact.v3.user.find_by_department(find_by_department_user_req) 55 | 56 | if not find_by_department_user_resp.success(): 57 | lark.logger.error( 58 | f"client.contact.v3.user.find_by_department failed, " 59 | f"code: {find_by_department_user_resp.code}, " 60 | f"msg: {find_by_department_user_resp.msg}, " 61 | f"log_id: {find_by_department_user_resp.get_log_id()}") 62 | return find_by_department_user_resp 63 | 64 | users.extend(find_by_department_user_resp.data.items) 65 | 66 | # 返回结果 67 | response = ListUserByDepartmentResponse() 68 | response.code = 0 69 | response.msg = "success" 70 | response.children_department_response = children_department_resp.data 71 | response.find_by_department_user_response = users 72 | 73 | return response 74 | -------------------------------------------------------------------------------- /composite_api/im/send_file.py: -------------------------------------------------------------------------------- 1 | """ 2 | 发送文件消息,使用到两个OpenAPI: 3 | 1. [上传文件](https://open.feishu.cn/document/server-docs/im-v1/file/create) 4 | 2. [发送消息](https://open.feishu.cn/document/server-docs/im-v1/message/create) 5 | """ 6 | 7 | import lark_oapi as lark 8 | from lark_oapi.api.im.v1 import * 9 | 10 | 11 | class SendFileRequest(object): 12 | 13 | def __init__(self) -> None: 14 | self.file_type: Optional[str] = None # 文件类型,必填 15 | self.file_name: Optional[str] = None # 带后缀的文件名,必填 16 | self.file: Optional[IO[Any]] = None # 文件内容,必填 17 | self.duration: Optional[int] = None # 文件的时长(ms),选填 18 | self.receive_id_type: Optional[str] = None # 消息接收者ID类型,必填 19 | self.receive_id: Optional[str] = None # 消息接收者的ID,必填 20 | self.uuid: Optional[str] = None # 消息uuid,选填 21 | 22 | 23 | class SendFileResponse(BaseResponse): 24 | def __init__(self) -> None: 25 | super().__init__() 26 | self.create_file_response: Optional[CreateFileResponseBody] = None 27 | self.create_message_response: Optional[CreateMessageResponseBody] = None 28 | 29 | 30 | # 发送文件消息 31 | def send_file(client: lark.Client, request: SendFileRequest) -> BaseResponse: 32 | # 上传文件 33 | create_file_req = CreateFileRequest.builder() \ 34 | .request_body(CreateFileRequestBody.builder() 35 | .file_type(request.file_type) 36 | .file_name(request.file_name) 37 | .duration(request.duration) 38 | .file(request.file) 39 | .build()) \ 40 | .build() 41 | 42 | create_file_resp = client.im.v1.file.create(create_file_req) 43 | 44 | if not create_file_resp.success(): 45 | lark.logger.error( 46 | f"client.im.v1.file.create failed, " 47 | f"code: {create_file_resp.code}, " 48 | f"msg: {create_file_resp.msg}, " 49 | f"log_id: {create_file_resp.get_log_id()}") 50 | return create_file_resp 51 | 52 | # 发送消息 53 | option = lark.RequestOption.builder().headers({"X-Tt-Logid": create_file_resp.get_log_id()}).build() 54 | create_message_req = CreateMessageRequest.builder() \ 55 | .receive_id_type(request.receive_id_type) \ 56 | .request_body(CreateMessageRequestBody.builder() 57 | .receive_id(request.receive_id) 58 | .msg_type("file") 59 | .content(lark.JSON.marshal(create_file_resp.data)) 60 | .uuid(request.uuid) 61 | .build()) \ 62 | .build() 63 | 64 | create_message_resp: CreateMessageResponse = client.im.v1.message.create(create_message_req, option) 65 | 66 | if not create_message_resp.success(): 67 | lark.logger.error( 68 | f"client.im.v1.message.create failed, " 69 | f"code: {create_message_resp.code}, " 70 | f"msg: {create_message_resp.msg}, " 71 | f"log_id: {create_message_resp.get_log_id()}") 72 | return create_message_resp 73 | 74 | # 返回结果 75 | response = SendFileResponse() 76 | response.code = 0 77 | response.msg = "success" 78 | response.create_file_response = create_file_resp.data 79 | response.create_message_response = create_message_resp.data 80 | 81 | return response 82 | -------------------------------------------------------------------------------- /composite_api/im/send_image.py: -------------------------------------------------------------------------------- 1 | """ 2 | 发送图片消息,使用到两个OpenAPI: 3 | 1. [上传图片](https://open.feishu.cn/document/server-docs/im-v1/image/create) 4 | 2. [发送消息](https://open.feishu.cn/document/server-docs/im-v1/message/create) 5 | """ 6 | 7 | import lark_oapi as lark 8 | from lark_oapi.api.im.v1 import * 9 | 10 | 11 | class SendImageRequest(object): 12 | 13 | def __init__(self) -> None: 14 | self.image: Optional[IO[Any]] = None # 图片,必填 15 | self.receive_id_type: Optional[str] = None # 消息接收者ID类型,必填 16 | self.receive_id: Optional[str] = None # 消息接收者的ID,必填 17 | self.uuid: Optional[str] = None # 消息uuid,选填 18 | 19 | 20 | class SendImageResponse(BaseResponse): 21 | def __init__(self) -> None: 22 | super().__init__() 23 | self.create_image_response: Optional[CreateImageResponseBody] = None 24 | self.create_message_response: Optional[CreateMessageResponseBody] = None 25 | 26 | 27 | # 发送图片消息 28 | def send_image(client: lark.Client, request: SendImageRequest) -> BaseResponse: 29 | # 上传图片 30 | create_image_req = CreateImageRequest.builder() \ 31 | .request_body(CreateImageRequestBody.builder() 32 | .image_type("message") 33 | .image(request.image) 34 | .build()) \ 35 | .build() 36 | 37 | create_image_resp = client.im.v1.image.create(create_image_req) 38 | 39 | if not create_image_resp.success(): 40 | lark.logger.error( 41 | f"client.im.v1.image.create failed, " 42 | f"code: {create_image_resp.code}, " 43 | f"msg: {create_image_resp.msg}, " 44 | f"log_id: {create_image_resp.get_log_id()}") 45 | return create_image_resp 46 | 47 | # 发送消息 48 | option = lark.RequestOption.builder().headers({"X-Tt-Logid": create_image_resp.get_log_id()}).build() 49 | create_message_req = CreateMessageRequest.builder() \ 50 | .receive_id_type(request.receive_id_type) \ 51 | .request_body(CreateMessageRequestBody.builder() 52 | .receive_id(request.receive_id) 53 | .msg_type("image") 54 | .content(lark.JSON.marshal(create_image_resp.data)) 55 | .uuid(request.uuid) 56 | .build()) \ 57 | .build() 58 | 59 | create_message_resp: CreateMessageResponse = client.im.v1.message.create(create_message_req, option) 60 | 61 | if not create_message_resp.success(): 62 | lark.logger.error( 63 | f"client.im.v1.message.create failed, " 64 | f"code: {create_message_resp.code}, " 65 | f"msg: {create_message_resp.msg}, " 66 | f"log_id: {create_message_resp.get_log_id()}") 67 | return create_message_resp 68 | 69 | # 返回结果 70 | response = SendImageResponse() 71 | response.code = 0 72 | response.msg = "success" 73 | response.create_image_response = create_image_resp.data 74 | response.create_message_response = create_message_resp.data 75 | 76 | return response 77 | -------------------------------------------------------------------------------- /composite_api/sheets/copy_and_paste_by_range.py: -------------------------------------------------------------------------------- 1 | """ 2 | 复制粘贴某个范围的单元格数据,使用到两个OpenAPI: 3 | 1. [读取单个范围](https://open.feishu.cn/document/server-docs/docs/sheets-v3/data-operation/reading-a-single-range) 4 | 2. [向单个范围写入数据](https://open.feishu.cn/document/server-docs/docs/sheets-v3/data-operation/write-data-to-a-single-range) 5 | """ 6 | import json 7 | from typing import Optional, Dict 8 | 9 | import lark_oapi as lark 10 | 11 | 12 | class CopyAndPasteByRangeRequest(object): 13 | def __init__(self) -> None: 14 | self.spreadsheetToken: Optional[str] = None # 表格token,必填 15 | self.src_range: Optional[str] = None # 来源范围,必填 16 | self.dst_range: Optional[str] = None # 目标范围,必填 17 | 18 | 19 | class CopyAndPasteRangeResponse(lark.BaseResponse): 20 | def __init__(self): 21 | super().__init__() 22 | self.read_response: Optional[Dict] = None 23 | self.write_response: Optional[Dict] = None 24 | 25 | 26 | # 复制粘贴某个范围的单元格数据 27 | def copy_and_paste_range(client: lark.Client, request: CopyAndPasteByRangeRequest) -> lark.BaseResponse: 28 | # 读取单个范围 29 | read_req: lark.BaseRequest = lark.BaseRequest.builder() \ 30 | .http_method(lark.HttpMethod.GET) \ 31 | .uri(f"/open-apis/sheets/v2/spreadsheets/{request.spreadsheetToken}/values/{request.src_range}") \ 32 | .token_types({lark.AccessTokenType.TENANT}) \ 33 | .build() 34 | 35 | read_resp = client.request(read_req) 36 | 37 | if not read_resp.success(): 38 | lark.logger.error( 39 | f"client.im.v1.message.create failed, " 40 | f"code: {read_resp.code}, " 41 | f"msg: {read_resp.msg}, " 42 | f"log_id: {read_resp.get_log_id()}") 43 | return read_resp 44 | 45 | # 向单个范围写入数据 46 | option = lark.RequestOption.builder().headers({"X-Tt-Logid": read_resp.get_log_id()}).build() 47 | read_data = json.loads(str(read_resp.raw.content, lark.UTF_8)).get("data") 48 | body = { 49 | "valueRange": { 50 | "range": request.dst_range, 51 | "values": read_data.get("valueRange").get("values"), 52 | } 53 | } 54 | write_req: lark.BaseRequest = lark.BaseRequest.builder() \ 55 | .http_method(lark.HttpMethod.PUT) \ 56 | .uri(f"/open-apis/sheets/v2/spreadsheets/{request.spreadsheetToken}/values") \ 57 | .token_types({lark.AccessTokenType.TENANT}) \ 58 | .body(body) \ 59 | .build() 60 | 61 | write_resp = client.request(write_req, option) 62 | 63 | if not write_resp.success(): 64 | lark.logger.error( 65 | f"client.im.v1.message.create failed, " 66 | f"code: {write_resp.code}, " 67 | f"msg: {write_resp.msg}, " 68 | f"log_id: {write_resp.get_log_id()}") 69 | return write_resp 70 | 71 | # 返回结果 72 | response = CopyAndPasteRangeResponse() 73 | response.code = 0 74 | response.msg = "success" 75 | response.read_response = read_data 76 | response.write_response = json.loads(str(write_resp.raw.content, lark.UTF_8)).get("data") 77 | 78 | return response 79 | -------------------------------------------------------------------------------- /composite_api/sheets/download_media_by_range.py: -------------------------------------------------------------------------------- 1 | """ 2 | 下载指定范围单元格的所有素材列表,使用到两个OpenAPI: 3 | 1. [读取单个范围](https://open.feishu.cn/document/server-docs/docs/sheets-v3/data-operation/reading-a-single-range) 4 | 2. [下载素材](https://open.feishu.cn/document/server-docs/docs/drive-v1/media/download) 5 | """ 6 | 7 | import json 8 | 9 | import lark_oapi as lark 10 | from lark_oapi.api.drive.v1 import * 11 | 12 | 13 | class DownloadMediaByRangeRequest(object): 14 | def __init__(self) -> None: 15 | self.spreadsheetToken: Optional[str] = None # 表格token,必填 16 | self.range: Optional[str] = None # 单元格范围,必填 17 | 18 | 19 | class DownloadMediaByRangeResponse(lark.BaseResponse): 20 | def __init__(self): 21 | super().__init__() 22 | self.read_response: Optional[Dict] = None 23 | self.download_media_response: List[DownloadMediaResponse] = [] 24 | 25 | 26 | # 下载指定范围单元格的所有素材列表 27 | def download_media_by_range(client: lark.Client, request: DownloadMediaByRangeRequest): 28 | # 读取单个范围 29 | read_req: lark.BaseRequest = lark.BaseRequest.builder() \ 30 | .http_method(lark.HttpMethod.GET) \ 31 | .uri(f"/open-apis/sheets/v2/spreadsheets/{request.spreadsheetToken}/values/{request.range}") \ 32 | .token_types({lark.AccessTokenType.TENANT}) \ 33 | .build() 34 | 35 | read_resp = client.request(read_req) 36 | 37 | if not read_resp.success(): 38 | lark.logger.error( 39 | f"client.im.v1.message.create failed, " 40 | f"code: {read_resp.code}, " 41 | f"msg: {read_resp.msg}, " 42 | f"log_id: {read_resp.get_log_id()}") 43 | return read_resp 44 | 45 | # 下载文件 46 | read_data = json.loads(str(read_resp.raw.content, lark.UTF_8)).get("data") 47 | tokens = _parse_file_token(read_data.get("valueRange").get("values"), []) 48 | option = lark.RequestOption.builder().headers({"X-Tt-Logid": read_resp.get_log_id()}).build() 49 | files = [] 50 | 51 | for token in tokens: 52 | download_media_req = DownloadMediaRequest.builder() \ 53 | .file_token(token) \ 54 | .build() 55 | 56 | download_media_resp = client.drive.v1.media.download(download_media_req, option) 57 | 58 | if not download_media_resp.success(): 59 | lark.logger.error( 60 | f"client.drive.v1.media.download failed, " 61 | f"code: {read_resp.code}, " 62 | f"msg: {read_resp.msg}, " 63 | f"log_id: {read_resp.get_log_id()}") 64 | return download_media_resp 65 | 66 | files.append(download_media_resp) 67 | 68 | # 返回结果 69 | response = DownloadMediaByRangeResponse() 70 | response.code = 0 71 | response.msg = "success" 72 | response.read_response = read_data 73 | response.download_media_response = files 74 | 75 | return response 76 | 77 | 78 | def _parse_file_token(values: List[Any], tokens: List[str]) -> List[str]: 79 | if values is None or len(values) == 0: 80 | return tokens 81 | for i in values: 82 | if isinstance(i, List): 83 | _parse_file_token(i, tokens) 84 | elif isinstance(i, dict) and "fileToken" in i: 85 | tokens.append(i.get("fileToken")) 86 | 87 | return tokens 88 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | APP_ID = "cli_xxxx" 2 | APP_SECRET = "xxxx" 3 | ENCRYPT_KEY = "xxxx" 4 | VERIFICATION_TOKEN = "xxxx" 5 | -------------------------------------------------------------------------------- /quick_start/robot/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larksuite/oapi-sdk-python-demo/770830122698ba26e3b64e2fd325cfcd9ad6065b/quick_start/robot/alert.png -------------------------------------------------------------------------------- /quick_start/robot/im.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import lark_oapi as lark 4 | from lark_oapi.api.im.v1 import * 5 | 6 | from client import client 7 | 8 | user_open_ids = ["ou_a79a0f82add14976e3943f4deb17c3fa", "ou_33c76a4cbeb76bd66608706edb32508e"] 9 | 10 | 11 | # 获取会话历史消息 12 | def list_chat_history(chat_id: str) -> None: 13 | request = ListMessageRequest.builder() \ 14 | .container_id_type("chat") \ 15 | .container_id(chat_id) \ 16 | .build() 17 | 18 | response = client.im.v1.message.list(request) 19 | 20 | if not response.success(): 21 | raise Exception( 22 | f"client.im.v1.message.list failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}") 23 | 24 | f = open(f"./chat_history.txt", "w") 25 | for i in response.data.items: 26 | sender_id = i.sender.id 27 | create_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(i.create_time) / 1000)) 28 | content = i.body.content 29 | 30 | msg = f"chatter({sender_id}) at {create_time} send: {content}" 31 | f.write(msg + "\n") 32 | 33 | f.close() 34 | 35 | 36 | # 创建报警群并拉人入群 37 | def create_alert_chat() -> str: 38 | request = CreateChatRequest.builder() \ 39 | .user_id_type("open_id") \ 40 | .request_body(CreateChatRequestBody.builder() 41 | .name("P0: 线上事故处理") 42 | .description("线上紧急事故处理") 43 | .user_id_list(user_open_ids) 44 | .build()) \ 45 | .build() 46 | 47 | response = client.im.v1.chat.create(request) 48 | 49 | if not response.success(): 50 | raise Exception( 51 | f"client.im.v1.chat.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}") 52 | 53 | return response.data.chat_id 54 | 55 | 56 | # 发送报警消息 57 | def send_alert_message(chat_id: str) -> None: 58 | request = CreateMessageRequest.builder() \ 59 | .receive_id_type("chat_id") \ 60 | .request_body(CreateMessageRequestBody.builder() 61 | .receive_id(chat_id) 62 | .msg_type("interactive") 63 | .content(_build_card("跟进处理")) 64 | .build()) \ 65 | .build() 66 | 67 | response = client.im.v1.chat.create(request) 68 | 69 | if not response.success(): 70 | raise Exception( 71 | f"client.im.v1.chat.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}") 72 | 73 | 74 | # 上传图片 75 | def _upload_image() -> str: 76 | file = open("alert.png", "rb") 77 | request = CreateImageRequest.builder() \ 78 | .request_body(CreateImageRequestBody.builder() 79 | .image_type("message") 80 | .image(file) 81 | .build()) \ 82 | .build() 83 | 84 | response = client.im.v1.image.create(request) 85 | 86 | if not response.success(): 87 | raise Exception( 88 | f"client.im.v1.image.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}") 89 | 90 | return response.data.image_key 91 | 92 | 93 | # 获取会话信息 94 | def get_chat_info(chat_id: str) -> GetChatResponseBody: 95 | request = GetChatRequest.builder() \ 96 | .chat_id(chat_id) \ 97 | .build() 98 | 99 | response = client.im.v1.chat.get(request) 100 | 101 | if not response.success(): 102 | raise Exception( 103 | f"client.im.v1.chat.get failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}") 104 | 105 | return response.data 106 | 107 | 108 | # 更新会话名称 109 | def update_chat_name(chat_id: str, chat_name: str): 110 | request: UpdateChatRequest = UpdateChatRequest.builder() \ 111 | .chat_id(chat_id) \ 112 | .request_body(UpdateChatRequestBody.builder() 113 | .name(chat_name) 114 | .build()) \ 115 | .build() 116 | 117 | response: UpdateChatResponse = client.im.v1.chat.update(request) 118 | 119 | if not response.success(): 120 | raise Exception( 121 | f"client.im.v1.chat.update failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}") 122 | 123 | 124 | # 处理消息回调 125 | def do_p2_im_message_receive_v1(data: P2ImMessageReceiveV1) -> None: 126 | msg = data.event.message 127 | if "/solve" in msg.content: 128 | request = CreateMessageRequest.builder() \ 129 | .receive_id_type("chat_id") \ 130 | .request_body(CreateMessageRequestBody.builder() 131 | .receive_id(msg.chat_id) 132 | .msg_type("text") 133 | .content("{\"text\":\"问题已解决,辛苦了!\"}") 134 | .build()) \ 135 | .build() 136 | 137 | response = client.im.v1.chat.create(request) 138 | 139 | if not response.success(): 140 | raise Exception( 141 | f"client.im.v1.chat.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}") 142 | 143 | # 获取会话信息 144 | chat_info = get_chat_info(msg.chat_id) 145 | name = chat_info.name 146 | if name.startswith("[跟进中]"): 147 | name = "[已解决]" + name[5:] 148 | elif not name.startswith("[已解决]"): 149 | name = "[已解决]" + name 150 | 151 | # 更新会话名称 152 | update_chat_name(msg.chat_id, name) 153 | 154 | 155 | # 处理卡片回调 156 | def do_interactive_card(data: lark.Card) -> Any: 157 | if data.action.value.get("key") == "follow": 158 | # 获取会话信息 159 | chat_info = get_chat_info(data.open_chat_id) 160 | name = chat_info.name 161 | if not name.startswith("[跟进中]") and not name.startswith("[已解决]"): 162 | name = "[跟进中] " + name 163 | 164 | # 更新会话名称 165 | update_chat_name(data.open_chat_id, name) 166 | 167 | return _build_card("跟进中") 168 | 169 | 170 | # 构建卡片 171 | def _build_card(button_name: str) -> str: 172 | image_key = _upload_image() 173 | card = { 174 | "config": { 175 | "wide_screen_mode": True 176 | }, 177 | "header": { 178 | "template": "red", 179 | "title": { 180 | "tag": "plain_text", 181 | "content": "1 级报警 - 数据平台" 182 | } 183 | }, 184 | "elements": [ 185 | { 186 | "tag": "div", 187 | "fields": [ 188 | { 189 | "is_short": True, 190 | "text": { 191 | "tag": "lark_md", 192 | "content": "**🕐 时间:**\n2021-02-23 20:17:51" 193 | } 194 | }, 195 | { 196 | "is_short": True, 197 | "text": { 198 | "tag": "lark_md", 199 | "content": "**🔢 事件 ID:**\n336720" 200 | } 201 | }, 202 | { 203 | "is_short": True, 204 | "text": { 205 | "tag": "lark_md", 206 | "content": "**📋 项目:**\nQA 7" 207 | } 208 | }, 209 | { 210 | "is_short": True, 211 | "text": { 212 | "tag": "lark_md", 213 | "content": "**👤 一级值班:**\n所有人" 214 | } 215 | }, 216 | { 217 | "is_short": True, 218 | "text": { 219 | "tag": "lark_md", 220 | "content": "**👤 二级值班:**\n所有人" 221 | } 222 | }, 223 | ] 224 | }, 225 | { 226 | "tag": "img", 227 | "img_key": image_key, 228 | "alt": { 229 | "tag": "plain_text", 230 | "content": " " 231 | }, 232 | "title": { 233 | "tag": "lark_md", 234 | "content": "支付方式 支付成功率低于 50%:" 235 | } 236 | }, 237 | { 238 | "tag": "note", 239 | "elements": [ 240 | { 241 | "tag": "plain_text", 242 | "content": "🔴 支付失败数 🔵 支付成功数" 243 | } 244 | ] 245 | }, 246 | { 247 | "tag": "action", 248 | "actions": [ 249 | { 250 | "tag": "button", 251 | "text": { 252 | "tag": "plain_text", 253 | "content": button_name 254 | }, 255 | "type": "primary", 256 | "value": { 257 | "key": "follow" 258 | }, 259 | }, 260 | { 261 | "tag": "select_static", 262 | "placeholder": { 263 | "tag": "plain_text", 264 | "content": "暂时屏蔽" 265 | }, 266 | "options": [ 267 | { 268 | "text": { 269 | "tag": "plain_text", 270 | "content": "屏蔽10分钟" 271 | }, 272 | "value": "1" 273 | }, 274 | { 275 | "text": { 276 | "tag": "plain_text", 277 | "content": "屏蔽30分钟" 278 | }, 279 | "value": "2" 280 | }, 281 | { 282 | "text": { 283 | "tag": "plain_text", 284 | "content": "屏蔽1小时" 285 | }, 286 | "value": "3" 287 | }, 288 | { 289 | "text": { 290 | "tag": "plain_text", 291 | "content": "屏蔽24小时" 292 | }, 293 | "value": "4" 294 | }, 295 | ], 296 | "value": { 297 | "key": "value" 298 | } 299 | } 300 | ] 301 | }, 302 | { 303 | "tag": "hr" 304 | }, 305 | { 306 | "tag": "div", 307 | "text": { 308 | "tag": "lark_md", 309 | "content": "🙋🏼 [我要反馈误报](https://open.feishu.cn/) | 📝 [录入报警处理过程](https://open.feishu.cn/)" 310 | } 311 | } 312 | ] 313 | } 314 | 315 | return lark.JSON.marshal(card) 316 | -------------------------------------------------------------------------------- /quick_start/robot/im_test.py: -------------------------------------------------------------------------------- 1 | from quick_start.robot.im import list_chat_history 2 | 3 | if __name__ == "__main__": 4 | list_chat_history("oc_e320c859f1e50cfbe4c35a33fca8a86f") 5 | -------------------------------------------------------------------------------- /quick_start/robot/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from lark_oapi.adapter.flask import * 3 | 4 | from config import * 5 | from quick_start.robot.im import * 6 | 7 | app = Flask(__name__) 8 | 9 | # 创建告警群并拉人入群 10 | chat_id = create_alert_chat() 11 | print(f"chat_id: {chat_id}") 12 | 13 | # 发送告警通知 14 | send_alert_message(chat_id) 15 | 16 | # 注册事件回调 17 | event_handler = lark.EventDispatcherHandler.builder(ENCRYPT_KEY, VERIFICATION_TOKEN, lark.LogLevel.DEBUG) \ 18 | .register_p2_im_message_receive_v1(do_p2_im_message_receive_v1) \ 19 | .build() 20 | 21 | # 注册卡片回调 22 | card_handler = lark.CardActionHandler.builder(ENCRYPT_KEY, VERIFICATION_TOKEN, lark.LogLevel.DEBUG) \ 23 | .register(do_interactive_card) \ 24 | .build() 25 | 26 | 27 | @app.route("/event", methods=["POST"]) 28 | def event(): 29 | resp = event_handler.do(parse_req()) 30 | return parse_resp(resp) 31 | 32 | 33 | @app.route("/card", methods=["POST"]) 34 | def card(): 35 | resp = card_handler.do(parse_req()) 36 | return parse_resp(resp) 37 | 38 | 39 | if __name__ == "__main__": 40 | app.run(port=7777) 41 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.6.2 2 | certifi==2023.7.22 3 | charset-normalizer==3.2.0 4 | click==8.1.6 5 | Flask==2.3.2 6 | idna==3.4 7 | importlib-metadata==6.8.0 8 | itsdangerous==2.1.2 9 | Jinja2==3.1.2 10 | lark-oapi==1.0.17 11 | MarkupSafe==2.1.3 12 | pycryptodome==3.18.0 13 | requests==2.31.0 14 | requests-toolbelt==1.0.0 15 | urllib3==2.0.4 16 | Werkzeug==2.3.7 17 | zipp==3.16.2 18 | -------------------------------------------------------------------------------- /tests/base/create_app_and_tables_test.py: -------------------------------------------------------------------------------- 1 | from client import client 2 | from composite_api.base.create_app_and_tables import * 3 | 4 | req = CreateAppAndTablesRequest() 5 | req.name = "这是多维表格" 6 | req.folder_token = "Y9LhfoWNZlKxWcdsf2fcPP0SnXc" 7 | req.tables = [ 8 | ReqTable.builder() 9 | .name("这是数据表1") 10 | .fields([AppTableCreateHeader.builder().field_name("field1").type(1).build(), 11 | AppTableCreateHeader.builder().field_name("field2").type(2).build()]) 12 | .build(), 13 | ReqTable.builder() 14 | .name("这是数据表2") 15 | .fields([AppTableCreateHeader.builder().field_name("field3").type(5).build(), 16 | AppTableCreateHeader.builder().field_name("field4").type(13).build()]) 17 | .build(), 18 | ] 19 | 20 | resp = create_app_and_tables(client, req) 21 | print(lark.JSON.marshal(resp, indent=4)) 22 | -------------------------------------------------------------------------------- /tests/contact/list_user_by_department_test.py: -------------------------------------------------------------------------------- 1 | from lark_oapi import JSON 2 | 3 | from client import client 4 | from composite_api.contact.list_user_by_department import ListUserByDepartmentRequest, list_user_by_department 5 | 6 | req = ListUserByDepartmentRequest() 7 | req.department_id = 0 8 | 9 | resp = list_user_by_department(client, req) 10 | print(JSON.marshal(resp, indent=4)) 11 | -------------------------------------------------------------------------------- /tests/im/send_file_test.py: -------------------------------------------------------------------------------- 1 | from client import client 2 | from composite_api.im.send_file import * 3 | 4 | req = SendFileRequest() 5 | req.file_type = "pdf" 6 | req.file_name = "demo.pdf" 7 | req.file = open("/Users/bytedance/Desktop/demo.pdf", "rb") 8 | req.receive_id_type = "open_id" 9 | req.receive_id = "ou_a79a0f82add14976e3943f4deb17c3fa" 10 | 11 | resp = send_file(client, req) 12 | print(lark.JSON.marshal(resp, indent=4)) 13 | -------------------------------------------------------------------------------- /tests/im/send_image_test.py: -------------------------------------------------------------------------------- 1 | from client import client 2 | from composite_api.im.send_image import * 3 | 4 | req = SendImageRequest() 5 | req.image = open("/Users/bytedance/Desktop/demo.png", "rb") 6 | req.receive_id_type = "open_id" 7 | req.receive_id = "ou_a79a0f82add14976e3943f4deb17c3fa" 8 | 9 | resp = send_image(client, req) 10 | print(lark.JSON.marshal(resp, indent=4)) 11 | -------------------------------------------------------------------------------- /tests/sheets/copy_and_paste_by_range_test.py: -------------------------------------------------------------------------------- 1 | from client import client 2 | from composite_api.sheets.copy_and_paste_by_range import * 3 | 4 | req = CopyAndPasteByRangeRequest() 5 | req.spreadsheetToken = "T90VsUqrYhrnGCtBKS3cLCgQnih" 6 | req.src_range = "53988e!A1:B5" 7 | req.dst_range = "53988e!C1:D5" 8 | 9 | resp = copy_and_paste_range(client, req) 10 | print(lark.JSON.marshal(resp, indent=4)) 11 | -------------------------------------------------------------------------------- /tests/sheets/download_media_by_range_test.py: -------------------------------------------------------------------------------- 1 | from client import client 2 | from composite_api.sheets.download_media_by_range import * 3 | 4 | req = DownloadMediaByRangeRequest() 5 | req.spreadsheetToken = "T90VsUqrYhrnGCtBKS3cLCgQnih" 6 | req.range = "53988e!A1:A7" 7 | 8 | resp = download_media_by_range(client, req) 9 | for i in resp.download_media_response: 10 | f = open(f"/Users/bytedance/Desktop/{i.file_name}", "wb") 11 | f.write(i.file.read()) 12 | f.close() 13 | 14 | --------------------------------------------------------------------------------