├── .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 |
--------------------------------------------------------------------------------