pytbox 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pytbox might be problematic. Click here for more details.
- pytbox/alicloud/sls.py +93 -0
- pytbox/common/__init__.py +7 -0
- pytbox/common/base.py +0 -0
- pytbox/dida365.py +297 -0
- pytbox/feishu/client.py +183 -0
- pytbox/feishu/endpoints.py +1049 -0
- pytbox/feishu/errors.py +45 -0
- pytbox/feishu/helpers.py +7 -0
- pytbox/feishu/typing.py +7 -0
- pytbox/logger.py +126 -0
- pytbox/onepassword_connect.py +92 -0
- pytbox/onepassword_sa.py +197 -0
- pytbox/utils/env.py +30 -0
- pytbox/utils/response.py +45 -0
- pytbox/utils/timeutils.py +40 -0
- pytbox/victorialog.py +125 -0
- pytbox/victoriametrics.py +37 -0
- pytbox-0.0.1.dist-info/METADATA +264 -0
- pytbox-0.0.1.dist-info/RECORD +21 -0
- pytbox-0.0.1.dist-info/WHEEL +5 -0
- pytbox-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import uuid
|
|
6
|
+
import time
|
|
7
|
+
import requests
|
|
8
|
+
import shelve
|
|
9
|
+
from typing import TYPE_CHECKING, Literal, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .client import BaseClient, FeishuResponse
|
|
13
|
+
from .helpers import pick
|
|
14
|
+
from requests_toolbelt import MultipartEncoder
|
|
15
|
+
from ..utils.response import ReturnResponse
|
|
16
|
+
|
|
17
|
+
class Endpoint:
|
|
18
|
+
|
|
19
|
+
def __init__(self, parent: "BaseClient") -> None:
|
|
20
|
+
self.parent = parent
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthEndpoint(Endpoint):
|
|
24
|
+
|
|
25
|
+
token_path = '/tmp/.feishu_token'
|
|
26
|
+
|
|
27
|
+
def save_token_to_file(self):
|
|
28
|
+
with shelve.open(self.token_path) as db:
|
|
29
|
+
db['token'] = self.refresh_access_token()
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
def fetch_token_from_file(self):
|
|
33
|
+
with shelve.open(self.token_path) as db:
|
|
34
|
+
token = db.get('token')
|
|
35
|
+
return token
|
|
36
|
+
|
|
37
|
+
def get_tenant_access_token(self):
|
|
38
|
+
'''
|
|
39
|
+
_summary_
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
_type_: _description_
|
|
43
|
+
'''
|
|
44
|
+
if os.environ.get('TENANT_ACCESS_TOKEN'):
|
|
45
|
+
return os.environ.get('TENANT_ACCESS_TOKEN')
|
|
46
|
+
else:
|
|
47
|
+
print('未找到token, 开始刷新')
|
|
48
|
+
resp = self.refresh_access_token()
|
|
49
|
+
if resp.tenant_access_token:
|
|
50
|
+
os.environ['TENANT_ACCESS_TOKEN'] = resp.tenant_access_token
|
|
51
|
+
return os.environ.get('TENANT_ACCESS_TOKEN')
|
|
52
|
+
|
|
53
|
+
def refresh_access_token(self):
|
|
54
|
+
payload = dict(
|
|
55
|
+
app_id=self.parent.app_id,
|
|
56
|
+
app_secret=self.parent.app_secret
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
token = requests.request(method='POST',
|
|
60
|
+
url=self.parent.options.base_url+'/auth/v3/tenant_access_token/internal',
|
|
61
|
+
json=payload, timeout=5).json()['tenant_access_token']
|
|
62
|
+
|
|
63
|
+
os.environ['TENANT_ACCESS_TOKEN'] = token
|
|
64
|
+
return token
|
|
65
|
+
|
|
66
|
+
class MessageEndpoint(Endpoint):
|
|
67
|
+
|
|
68
|
+
def send_text(self,
|
|
69
|
+
text: str,
|
|
70
|
+
receive_id: str):
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
format_message_content = json.dumps({ "text": text }, ensure_ascii=False)
|
|
74
|
+
|
|
75
|
+
payload = {
|
|
76
|
+
"content": format_message_content,
|
|
77
|
+
"msg_type": "text",
|
|
78
|
+
"receive_id": receive_id,
|
|
79
|
+
"uuid": str(uuid.uuid4())
|
|
80
|
+
}
|
|
81
|
+
receive_id_type = self.parent.extensions.parse_receive_id_type(receive_id=receive_id)
|
|
82
|
+
|
|
83
|
+
return self.parent.request(path=f'/im/v1/messages?receive_id_type={receive_id_type}',
|
|
84
|
+
method='POST',
|
|
85
|
+
body=payload)
|
|
86
|
+
|
|
87
|
+
def send_post(self,
|
|
88
|
+
receive_id: str=None,
|
|
89
|
+
message_id: str=None,
|
|
90
|
+
title: str=None,
|
|
91
|
+
content: list=None):
|
|
92
|
+
'''
|
|
93
|
+
发送富文本消息
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
reveive_id (str): 必选参数, 接收消息的 id, 可以是 chat_id, 也可以是 openid, 代码会自动判断
|
|
97
|
+
message_id (str): 如果设置此参数, 表示会在原消息上回复消息
|
|
98
|
+
title: (str): 消息的标题
|
|
99
|
+
content: (list): 消息的内容, 示例格式如下
|
|
100
|
+
content = [
|
|
101
|
+
[
|
|
102
|
+
{"tag": "text", "text": "VPN: XXX:8443"}
|
|
103
|
+
]
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
response (dict): 返回发送消息后的响应, 是一个大的 json, 还在考虑是否拆分一下
|
|
108
|
+
'''
|
|
109
|
+
|
|
110
|
+
message_content = {
|
|
111
|
+
"zh_cn": {
|
|
112
|
+
"title": title,
|
|
113
|
+
"content": content
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
format_message_content = json.dumps(message_content, ensure_ascii=False)
|
|
118
|
+
|
|
119
|
+
if receive_id:
|
|
120
|
+
receive_id_type = self.parent.extensions.parse_receive_id_type(receive_id=receive_id)
|
|
121
|
+
api = f'/im/v1/messages?receive_id_type={receive_id_type}'
|
|
122
|
+
payload = {
|
|
123
|
+
"content": format_message_content,
|
|
124
|
+
"msg_type": "post",
|
|
125
|
+
"receive_id": receive_id,
|
|
126
|
+
"uuid": str(uuid.uuid4())
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
elif message_id:
|
|
130
|
+
api = f'/im/v1/messages/{message_id}/reply'
|
|
131
|
+
payload = {
|
|
132
|
+
"content": format_message_content,
|
|
133
|
+
"msg_type": "post",
|
|
134
|
+
"uuid": str(uuid.uuid4())
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return self.parent.request(path=f'/im/v1/messages?receive_id_type={receive_id_type}',
|
|
138
|
+
method='POST',
|
|
139
|
+
body=payload)
|
|
140
|
+
|
|
141
|
+
def send_card(self, template_id: str, template_variable: dict=None, receive_id: str=None):
|
|
142
|
+
'''
|
|
143
|
+
目前主要使用的发送卡片消息的函数, 从名字可以看出, 这是第2代的发送消息卡片函数
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
template_id (str): 消息卡片的 id, 可以在飞书的消息卡片搭建工具中获得该 id
|
|
147
|
+
template_variable (dict): 消息卡片中的变量
|
|
148
|
+
receive_id: (str): 接收消息的 id, 可以填写 open_id、chat_id, 函数会自动检测
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
response (dict): 返回发送消息后的响应, 是一个大的 json, 还在考虑是否拆分一下
|
|
152
|
+
'''
|
|
153
|
+
receive_id_type = self.parent.extensions.parse_receive_id_type(receive_id=receive_id)
|
|
154
|
+
content = {
|
|
155
|
+
"type":"template",
|
|
156
|
+
"data":{
|
|
157
|
+
"template_id": template_id,
|
|
158
|
+
"template_variable": template_variable
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
content = json.dumps(content, ensure_ascii=False)
|
|
163
|
+
|
|
164
|
+
payload = {
|
|
165
|
+
"content": content,
|
|
166
|
+
"msg_type": "interactive",
|
|
167
|
+
"receive_id": receive_id
|
|
168
|
+
}
|
|
169
|
+
return self.parent.request(path=f'/im/v1/messages?receive_id_type={receive_id_type}',
|
|
170
|
+
method='POST',
|
|
171
|
+
body=payload)
|
|
172
|
+
|
|
173
|
+
def send_file(self, file_name, file_path, receive_id):
|
|
174
|
+
receive_id_type = self.parent.extensions.parse_receive_id_type(receive_id=receive_id)
|
|
175
|
+
content = {
|
|
176
|
+
"file_key": self.parent.extensions.upload_file(file_name=file_name, file_path=file_path)
|
|
177
|
+
}
|
|
178
|
+
content = json.dumps(content, ensure_ascii=False)
|
|
179
|
+
payload = {
|
|
180
|
+
"content": content,
|
|
181
|
+
"msg_type": "file",
|
|
182
|
+
"receive_id": receive_id
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return self.parent.request(path=f'/im/v1/messages?receive_id_type={receive_id_type}',
|
|
186
|
+
method='POST',
|
|
187
|
+
body=payload)
|
|
188
|
+
|
|
189
|
+
def get_history(self, chat_id: str=None, chat_type: Literal['chat', 'thread']='chat', start_time: int=int(time.time())-300, end_time: int=int(time.time()), last_minute: int=5, page_size: int=50):
|
|
190
|
+
'''
|
|
191
|
+
_summary_
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
chat_id (str, optional): _description_. Defaults to None.
|
|
195
|
+
chat_type (Literal['chat', 'thread'], optional): _description_. Defaults to 'chat'.
|
|
196
|
+
start_time (int, optional): _description_. Defaults to int(time.time())-300.
|
|
197
|
+
end_time (int, optional): _description_. Defaults to int(time.time()).
|
|
198
|
+
page_size (int, optional): _description_. Defaults to 50.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
_type_: _description_
|
|
202
|
+
'''
|
|
203
|
+
start_time = int(time.time()) - last_minute * 60
|
|
204
|
+
return self.parent.request(path=f'/im/v1/messages?container_id={chat_id}&container_id_type={chat_type}&end_time={end_time}&page_size={page_size}&sort_type=ByCreateTimeAsc&start_time={start_time}',
|
|
205
|
+
method='GET')
|
|
206
|
+
|
|
207
|
+
def reply(self, message_id, content):
|
|
208
|
+
content = {
|
|
209
|
+
"text": content
|
|
210
|
+
}
|
|
211
|
+
payload = {
|
|
212
|
+
"content": json.dumps(content, ensure_ascii=False),
|
|
213
|
+
"msg_type": "text",
|
|
214
|
+
"reply_in_thread": False,
|
|
215
|
+
"uuid": str(uuid.uuid4())
|
|
216
|
+
}
|
|
217
|
+
return self.parent.request(
|
|
218
|
+
path=f"https://open.feishu.cn/open-apis/im/v1/messages/{message_id}/reply",
|
|
219
|
+
method='POST',
|
|
220
|
+
body=payload
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def forward(self, message_id, receive_id):
|
|
224
|
+
receive_id_type = self.parent.extensions.parse_receive_id_type(receive_id=receive_id)
|
|
225
|
+
payload = {
|
|
226
|
+
"receive_id": receive_id
|
|
227
|
+
}
|
|
228
|
+
return self.parent.request(
|
|
229
|
+
path=f"/im/v1/messages/{message_id}/forward?receive_id_type={receive_id_type}",
|
|
230
|
+
method='POST',
|
|
231
|
+
body=payload
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def emoji(self, message_id, emoji_type: Literal['DONE', 'ERROR', 'SPITBLOOD', 'LIKE', 'LOVE', 'CARE', 'WOW', 'SAD', 'ANGRY', 'SILENT']) -> ReturnResponse:
|
|
235
|
+
'''
|
|
236
|
+
表情文案说明: https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
message_id (_type_): _description_
|
|
240
|
+
emoji_type (str): _description_
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
_type_: _description_
|
|
244
|
+
'''
|
|
245
|
+
payload = {
|
|
246
|
+
"reaction_type": {
|
|
247
|
+
"emoji_type": emoji_type
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
r = self.parent.request(
|
|
252
|
+
path=f"im/v1/messages/{message_id}/reactions",
|
|
253
|
+
method='POST',
|
|
254
|
+
body=payload
|
|
255
|
+
)
|
|
256
|
+
if r.code == 0:
|
|
257
|
+
return ReturnResponse(code=0, message=f"{message_id} 回复 emoji [{emoji_type}] 成功")
|
|
258
|
+
else:
|
|
259
|
+
return ReturnResponse(code=1, message=f"{message_id} 回复 emoji [{emoji_type}] 失败")
|
|
260
|
+
|
|
261
|
+
class BitableEndpoint(Endpoint):
|
|
262
|
+
|
|
263
|
+
def list_records(self, app_token, table_id, field_names: list=None, automatic_fields: bool=False, filter_conditions: list=None, conjunction: Literal['and', 'or']='and', sort_field_name: str=None, view_id: str=None):
|
|
264
|
+
'''
|
|
265
|
+
如果是多维表格中的表格, 需要先获取 app_token
|
|
266
|
+
https://open.feishu.cn/document/server-docs/docs/wiki-v2/space-node/get_node?appId=cli_a1ae749cd7f9100d
|
|
267
|
+
|
|
268
|
+
参考文档: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
app_token (_type_): obj_token
|
|
272
|
+
table_id (_type_): _description_
|
|
273
|
+
filter_conditions (_type_): https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/record-filter-guide
|
|
274
|
+
'''
|
|
275
|
+
payload = {
|
|
276
|
+
"automatic_fields": automatic_fields,
|
|
277
|
+
"field_names": field_names,
|
|
278
|
+
"filter": {
|
|
279
|
+
"conditions": filter_conditions,
|
|
280
|
+
"conjunction": conjunction
|
|
281
|
+
},
|
|
282
|
+
"view_id": view_id
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if sort_field_name:
|
|
286
|
+
payload['sort'] = [
|
|
287
|
+
{
|
|
288
|
+
"desc": True,
|
|
289
|
+
"field_name": sort_field_name
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
records = self.parent.request(path=f'/bitable/v1/apps/{app_token}/tables/{table_id}/records/search', method='POST', body=payload)
|
|
294
|
+
if records.code == 0:
|
|
295
|
+
new_dict = {}
|
|
296
|
+
for item in records.data['items']:
|
|
297
|
+
for key, value in item['fields'].items():
|
|
298
|
+
if isinstance(value, list):
|
|
299
|
+
try:
|
|
300
|
+
value = value[0].get('text')
|
|
301
|
+
except AttributeError:
|
|
302
|
+
pass
|
|
303
|
+
new_dict[key] = value
|
|
304
|
+
yield new_dict
|
|
305
|
+
elif records.code != 0:
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
def add_record(self, app_token, table_id, fields):
|
|
309
|
+
payload = {
|
|
310
|
+
"fields": fields
|
|
311
|
+
}
|
|
312
|
+
return self.parent.request(path=f'/bitable/v1/apps/{app_token}/tables/{table_id}/records',
|
|
313
|
+
method='POST',
|
|
314
|
+
body=payload)
|
|
315
|
+
|
|
316
|
+
def query_record(self, app_token: str=None, table_id: str=None, automatic_fields: bool=False, field_names: list=None, filter_conditions: list=None, conjunction: Literal['and', 'or']='and', sort_field_name: str=None, view_id: str=None):
|
|
317
|
+
'''
|
|
318
|
+
https://open.feishu.cn/api-explorer/cli_a1ae749cd7f9100d?apiName=search&from=op_doc_tab&project=bitable&resource=app.table.record&version=v1
|
|
319
|
+
Args:
|
|
320
|
+
app_token (_type_): _description_
|
|
321
|
+
table_id (_type_): _description_
|
|
322
|
+
view_id (_type_): _description_
|
|
323
|
+
automatic_fields (_type_): _description_
|
|
324
|
+
field_names (_type_): _description_
|
|
325
|
+
filter_conditions (_type_): [
|
|
326
|
+
{
|
|
327
|
+
"field_name": "职位",
|
|
328
|
+
"operator": "is",
|
|
329
|
+
"value": [
|
|
330
|
+
"初级销售员"
|
|
331
|
+
]
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"field_name": "销售额",
|
|
335
|
+
"operator": "isGreater",
|
|
336
|
+
"value": [
|
|
337
|
+
"10000.0"
|
|
338
|
+
]
|
|
339
|
+
}
|
|
340
|
+
],
|
|
341
|
+
conjunction (_type_): _description_
|
|
342
|
+
sort_field_name (_type_): _description_
|
|
343
|
+
view_id (_type_): _description_
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
_type_: _description_
|
|
347
|
+
'''
|
|
348
|
+
payload = {
|
|
349
|
+
"automatic_fields": automatic_fields,
|
|
350
|
+
"field_names": field_names,
|
|
351
|
+
"filter": {
|
|
352
|
+
"conditions": filter_conditions,
|
|
353
|
+
"conjunction": conjunction
|
|
354
|
+
},
|
|
355
|
+
"view_id": view_id
|
|
356
|
+
}
|
|
357
|
+
if sort_field_name:
|
|
358
|
+
payload['sort'] = [
|
|
359
|
+
{
|
|
360
|
+
"desc": True,
|
|
361
|
+
"field_name": sort_field_name
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
return self.parent.request(path=f'/bitable/v1/apps/{app_token}/tables/{table_id}/records/search',
|
|
365
|
+
method='POST',
|
|
366
|
+
body=payload)
|
|
367
|
+
|
|
368
|
+
def query_record_id(self,
|
|
369
|
+
app_token: str=None,
|
|
370
|
+
table_id: str=None, filter_field_name: str=None, filter_value: str=None) -> str | None:
|
|
371
|
+
'''
|
|
372
|
+
用于单向或双向关联
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
app_token (str, optional): _description_. Defaults to None.
|
|
376
|
+
table_id (str, optional): _description_. Defaults to None.
|
|
377
|
+
filter_field_name (str, optional): _description_. Defaults to None.
|
|
378
|
+
filter_value (str, optional): _description_. Defaults to None.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
str | None: _description_
|
|
382
|
+
'''
|
|
383
|
+
payload = {
|
|
384
|
+
"automatic_fields": False,
|
|
385
|
+
"filter": {
|
|
386
|
+
"conditions": [
|
|
387
|
+
{
|
|
388
|
+
"field_name": filter_field_name,
|
|
389
|
+
"operator": "is",
|
|
390
|
+
"value": [filter_value]
|
|
391
|
+
}
|
|
392
|
+
],
|
|
393
|
+
"conjunction": "and"
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
res = self.parent.request(path=f'/bitable/v1/apps/{app_token}/tables/{table_id}/records/search',
|
|
397
|
+
method='POST',
|
|
398
|
+
body=payload)
|
|
399
|
+
if res.code == 0:
|
|
400
|
+
try:
|
|
401
|
+
return res.data['items'][0]['record_id']
|
|
402
|
+
except IndexError:
|
|
403
|
+
return None
|
|
404
|
+
else:
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
def add_and_update_record(self,
|
|
408
|
+
app_token: str=None,
|
|
409
|
+
table_id: str=None,
|
|
410
|
+
record_id: str=None,
|
|
411
|
+
fields: dict=None,
|
|
412
|
+
filter_field_name: str=None,
|
|
413
|
+
filter_value: str=None) -> ReturnResponse:
|
|
414
|
+
'''
|
|
415
|
+
_summary_
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
app_token (_type_): _description_
|
|
419
|
+
table_id (_type_): _description_
|
|
420
|
+
record_id (_type_): _description_
|
|
421
|
+
fields (_type_): _description_
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
ReturnResponse: _description_
|
|
425
|
+
'''
|
|
426
|
+
record_id = self.query_record_id(app_token, table_id, filter_field_name, filter_value)
|
|
427
|
+
|
|
428
|
+
if record_id:
|
|
429
|
+
payload = {
|
|
430
|
+
"fields": {k: v for k, v in fields.items() if v is not None}
|
|
431
|
+
}
|
|
432
|
+
resp = self.parent.request(path=f'/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}',
|
|
433
|
+
method='PUT',
|
|
434
|
+
body=payload)
|
|
435
|
+
return ReturnResponse(code=resp.code, message=f"记录已存在, 进行更新", data=resp.data)
|
|
436
|
+
else:
|
|
437
|
+
resp = self.add_record(app_token, table_id, fields)
|
|
438
|
+
return ReturnResponse(code=resp.code, message=f"记录不存在, 进行创建", data=resp.data)
|
|
439
|
+
|
|
440
|
+
def query_name_by_record_id(self, app_token: str=None, table_id: str=None, field_names: list=None, record_id: str='', name: str=''):
|
|
441
|
+
response = self.query_record(app_token=app_token, table_id=table_id, field_names=field_names)
|
|
442
|
+
if response.code == 0:
|
|
443
|
+
for item in response.data['items']:
|
|
444
|
+
if item['record_id'] == record_id:
|
|
445
|
+
# print(item['fields'])
|
|
446
|
+
return self.parent.extensions.parse_bitable_data(item['fields'], name)
|
|
447
|
+
# ss
|
|
448
|
+
else:
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
class DocsEndpoint(Endpoint):
|
|
452
|
+
|
|
453
|
+
def rename_doc_title(self, space_id, node_token, title):
|
|
454
|
+
payload = {
|
|
455
|
+
"title": title
|
|
456
|
+
}
|
|
457
|
+
return self.parent.request(path=f'/wiki/v2/spaces/{space_id}/nodes/{node_token}/update_title',
|
|
458
|
+
method='POST',
|
|
459
|
+
body=payload)
|
|
460
|
+
|
|
461
|
+
def create_doc(self, space_id: str, parent_node_token: str, title: str):
|
|
462
|
+
'''
|
|
463
|
+
在知识库中创建文档
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
space_id (_type_): 知识库的 id
|
|
467
|
+
parent_node_token (_type_): 父节点 token, 通过浏览器的链接可以获取, 例如 https://tyun.feishu.cn/wiki/J4tjweM5xiCBADk1zo7c6wXOnHO
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
_type_: document.id: res.data['node']['obj_token']
|
|
471
|
+
'''
|
|
472
|
+
payload = {
|
|
473
|
+
"node_type": "origin",
|
|
474
|
+
"obj_type": "docx",
|
|
475
|
+
"parent_node_token": parent_node_token
|
|
476
|
+
}
|
|
477
|
+
res = self.parent.request(path=f'/wiki/v2/spaces/{space_id}/nodes',
|
|
478
|
+
method='POST',
|
|
479
|
+
body=payload)
|
|
480
|
+
if res.code == 0:
|
|
481
|
+
self.rename_doc_title(space_id=space_id, node_token=res.data['node']['node_token'], title=title)
|
|
482
|
+
return res
|
|
483
|
+
else:
|
|
484
|
+
return res
|
|
485
|
+
|
|
486
|
+
def create_block(self, document_id: str=None, block_id: str=None, client_token: str=None, payload: dict={}):
|
|
487
|
+
'''
|
|
488
|
+
_summary_
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
document_id (str, optional): _description_. Defaults to None.
|
|
492
|
+
block_id (str, optional): _description_. Defaults to None.
|
|
493
|
+
client_token (str, optional): _description_. Defaults to None.
|
|
494
|
+
children (list, optional): _description_. Defaults to None.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
_type_: _description_
|
|
498
|
+
'''
|
|
499
|
+
if payload.get('children_id'):
|
|
500
|
+
# 创建嵌套块, 参考文档
|
|
501
|
+
# https://open.feishu.cn/api-explorer/cli_a1ae749cd7f9100d?apiName=create&from=op_doc_tab&project=docx&resource=document.block.descendant&version=v1
|
|
502
|
+
return self.parent.request(path=f'/docx/v1/documents/{document_id}/blocks/{block_id}/descendant',
|
|
503
|
+
method='POST',
|
|
504
|
+
body=payload)
|
|
505
|
+
else:
|
|
506
|
+
return self.parent.request(path=f'/docx/v1/documents/{document_id}/blocks/{block_id}/children',
|
|
507
|
+
method='POST',
|
|
508
|
+
body=payload)
|
|
509
|
+
|
|
510
|
+
def create_block_children(self, document_id: str=None, block_id: str=None, payload: dict=None):
|
|
511
|
+
return self.parent.request(path=f'/docx/v1/documents/{document_id}/blocks/{block_id}/children',
|
|
512
|
+
method='POST',
|
|
513
|
+
body=payload)
|
|
514
|
+
|
|
515
|
+
def update_block(self, document_id: str=None, block_id: str=None, replace_image_token: str=None, image_width: int=100, image_height: int=100, image_align: int=2):
|
|
516
|
+
payload = {}
|
|
517
|
+
if replace_image_token:
|
|
518
|
+
payload['replace_image'] = {
|
|
519
|
+
'token': replace_image_token,
|
|
520
|
+
'width': image_width,
|
|
521
|
+
'height': image_height,
|
|
522
|
+
'align': image_align
|
|
523
|
+
}
|
|
524
|
+
return self.parent.request(path=f'/docx/v1/documents/{document_id}/blocks/{block_id}',
|
|
525
|
+
method='PATCH',
|
|
526
|
+
body=payload)
|
|
527
|
+
|
|
528
|
+
class CalendarEndpoint(Endpoint):
|
|
529
|
+
def get_events(self,
|
|
530
|
+
calendar_id: str='feishu.cn_dQ4cLmSfGa1QSWqv3EvpLf@group.calendar.feishu.cn',
|
|
531
|
+
start_time: int=int(time.time()) - 30*24*60*60,
|
|
532
|
+
end_time: int=int(time.time()),
|
|
533
|
+
page_size: int=500,
|
|
534
|
+
anchor_time: int=None
|
|
535
|
+
):
|
|
536
|
+
if anchor_time:
|
|
537
|
+
anchor_time = f'&anchor_time={anchor_time}'
|
|
538
|
+
else:
|
|
539
|
+
anchor_time = ''
|
|
540
|
+
return self.parent.request(path=f'/calendar/v4/calendars/{calendar_id}/events?anchor_time={start_time}&end_time={end_time}&page_size={page_size}&start_time={start_time}',
|
|
541
|
+
method='GET')
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
class ExtensionsEndpoint(Endpoint):
|
|
545
|
+
def parse_receive_id_type(self, receive_id):
|
|
546
|
+
if receive_id.startswith('ou'):
|
|
547
|
+
receive_id_type = 'open_id'
|
|
548
|
+
elif receive_id.startswith('oc'):
|
|
549
|
+
receive_id_type = 'chat_id'
|
|
550
|
+
else:
|
|
551
|
+
raise ValueError('No such named receive_id')
|
|
552
|
+
return receive_id_type
|
|
553
|
+
|
|
554
|
+
def upload_file(self, file_name, file_path):
|
|
555
|
+
|
|
556
|
+
files = {
|
|
557
|
+
'file_type': ('', 'stream'),
|
|
558
|
+
'file_name': ('', file_name),
|
|
559
|
+
'file': open(file_path, 'rb')
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return self.parent.request(path='/im/v1/files',
|
|
563
|
+
method='POST',
|
|
564
|
+
files=files).data['file_key']
|
|
565
|
+
|
|
566
|
+
def upload_image(self, image_path):
|
|
567
|
+
import requests
|
|
568
|
+
from requests_toolbelt import MultipartEncoder
|
|
569
|
+
|
|
570
|
+
url = "https://open.feishu.cn/open-apis/im/v1/images"
|
|
571
|
+
|
|
572
|
+
form = {
|
|
573
|
+
'image_type': 'message',
|
|
574
|
+
'image': (open(image_path, 'rb'))
|
|
575
|
+
} # 需要替换具体的path
|
|
576
|
+
|
|
577
|
+
multi_form = MultipartEncoder(form)
|
|
578
|
+
if self.parent.auth.fetch_token_from_file():
|
|
579
|
+
token = self.parent.auth.fetch_token_from_file()
|
|
580
|
+
else:
|
|
581
|
+
self.parent.auth.save_token_to_file()
|
|
582
|
+
token = self.parent.auth.fetch_token_from_file()
|
|
583
|
+
headers = {
|
|
584
|
+
'Authorization': f'Bearer {token}', ## 获取tenant_access_token, 需要替换为实际的token
|
|
585
|
+
}
|
|
586
|
+
headers['Content-Type'] = multi_form.content_type
|
|
587
|
+
response = requests.request("POST", url, headers=headers, data=multi_form)
|
|
588
|
+
response_json = response.json()
|
|
589
|
+
if response_json['code'] == 0:
|
|
590
|
+
return response_json['data']['image_key']
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def build_block_heading(self, content, heading_level: Literal[1, 2, 3, 4]):
|
|
594
|
+
return {
|
|
595
|
+
"index": 0,
|
|
596
|
+
"children": [
|
|
597
|
+
{
|
|
598
|
+
"block_type": heading_level + 2,
|
|
599
|
+
f"heading{heading_level}": {
|
|
600
|
+
"elements": [
|
|
601
|
+
{
|
|
602
|
+
"text_run": {
|
|
603
|
+
"content": content
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
]
|
|
607
|
+
},
|
|
608
|
+
"style": {}
|
|
609
|
+
}
|
|
610
|
+
]
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
def build_block_element(self, content: str=None, background_color: int=None, text_color: int=None):
|
|
614
|
+
element = {
|
|
615
|
+
"text_run": {
|
|
616
|
+
"content": content,
|
|
617
|
+
"text_element_style": {}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if background_color:
|
|
622
|
+
element['text_run']['text_element_style']['background_color'] = background_color
|
|
623
|
+
|
|
624
|
+
if text_color:
|
|
625
|
+
element['text_run']['text_element_style']['text_color'] = text_color
|
|
626
|
+
|
|
627
|
+
return element
|
|
628
|
+
|
|
629
|
+
def build_block_text(self, elements: list=None) -> dict:
|
|
630
|
+
'''
|
|
631
|
+
构建飞书文档文本块。
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
elements (list, optional): 请使用 build_block_element 函数构建元素
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
dict: 飞书文档文本块
|
|
638
|
+
'''
|
|
639
|
+
return {
|
|
640
|
+
"index": 0,
|
|
641
|
+
"children": [
|
|
642
|
+
{
|
|
643
|
+
"block_type": 2,
|
|
644
|
+
"text": {
|
|
645
|
+
"elements": elements,
|
|
646
|
+
"style": {}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
]
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
def build_block_bullet(self, content_list: list = None, background_color: int=None, text_color: int=None) -> dict:
|
|
653
|
+
"""
|
|
654
|
+
构建飞书文档项目符号列表块。
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
content_list (list, optional): 内容列表,将批量添加到 children 中
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
dict: 飞书文档项目符号列表块
|
|
661
|
+
"""
|
|
662
|
+
children = []
|
|
663
|
+
|
|
664
|
+
for content in content_list:
|
|
665
|
+
children.append({
|
|
666
|
+
"block_type": 12,
|
|
667
|
+
"bullet": {
|
|
668
|
+
"elements": [
|
|
669
|
+
self.build_block_element(content=content, background_color=background_color, text_color=text_color)
|
|
670
|
+
]
|
|
671
|
+
}
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
"index": 0,
|
|
676
|
+
"children": children
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
def build_block_callout(self, content: str=None, background_color: int=1, border_color: int=2, text_color: int=5, emoji_id: str='grinning', bold: bool=False):
|
|
680
|
+
'''
|
|
681
|
+
_summary_
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
content (str, optional): _description_. Defaults to None.
|
|
685
|
+
background_color (int, optional): _description_. Defaults to 1.
|
|
686
|
+
border_color (int, optional): _description_. Defaults to 2.
|
|
687
|
+
text_color (int, optional): _description_. Defaults to 5.
|
|
688
|
+
emoji_id (str, optional): _description_. Defaults to 'grinning'.
|
|
689
|
+
bold (bool, optional): _description_. Defaults to False.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
_type_: _description_
|
|
693
|
+
'''
|
|
694
|
+
return {
|
|
695
|
+
"index": 0,
|
|
696
|
+
"children_id": [
|
|
697
|
+
"callout1",
|
|
698
|
+
],
|
|
699
|
+
"descendants": [
|
|
700
|
+
{
|
|
701
|
+
"block_id": "callout1",
|
|
702
|
+
"block_type": 19,
|
|
703
|
+
"callout": {
|
|
704
|
+
"background_color": background_color,
|
|
705
|
+
"border_color": border_color,
|
|
706
|
+
"text_color": text_color,
|
|
707
|
+
"emoji_id": emoji_id
|
|
708
|
+
},
|
|
709
|
+
"children": [
|
|
710
|
+
"text1",
|
|
711
|
+
]
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
"block_id": "text1",
|
|
715
|
+
"block_type": 2,
|
|
716
|
+
"text": {
|
|
717
|
+
"elements": [
|
|
718
|
+
{
|
|
719
|
+
"text_run": {
|
|
720
|
+
"content": content,
|
|
721
|
+
"text_element_style": {
|
|
722
|
+
"bold": bold
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
]
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
]
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
def build_block_table(self, rows: int=1, columns: int=1, column_width: list=[], data=None):
|
|
733
|
+
"""
|
|
734
|
+
构建飞书文档表格块
|
|
735
|
+
参考文档: https://open.feishu.cn/document/docs/docs/faq
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
rows: 表格行数
|
|
739
|
+
columns: 表格列数
|
|
740
|
+
data: 表格数据,可以是二维列表[[cell1, cell2], [cell3, cell4]]
|
|
741
|
+
或者单元格块ID的列表
|
|
742
|
+
|
|
743
|
+
Returns:
|
|
744
|
+
dict: 符合飞书文档API要求的表格结构
|
|
745
|
+
"""
|
|
746
|
+
# 生成表格ID和单元格ID
|
|
747
|
+
table_id = f"table_{uuid.uuid4().hex[:8]}"
|
|
748
|
+
cell_ids = []
|
|
749
|
+
cell_blocks = []
|
|
750
|
+
|
|
751
|
+
# if data:
|
|
752
|
+
# # 在data列表末尾添加一条新数据
|
|
753
|
+
# data.append(['sss'] * columns) # 添加一个空行
|
|
754
|
+
|
|
755
|
+
# print(data)
|
|
756
|
+
|
|
757
|
+
# 生成单元格ID和块
|
|
758
|
+
for row in range(rows):
|
|
759
|
+
row_cells = []
|
|
760
|
+
for col in range(columns):
|
|
761
|
+
cell_id = f"cell_{row}_{col}_{uuid.uuid4().hex[:4]}"
|
|
762
|
+
row_cells.append(cell_id)
|
|
763
|
+
|
|
764
|
+
# 获取单元格内容
|
|
765
|
+
cell_content = ""
|
|
766
|
+
if data and len(data) > row and isinstance(data[row], (list, tuple)) and len(data[row]) > col:
|
|
767
|
+
cell_content = data[row][col]
|
|
768
|
+
|
|
769
|
+
# 创建单元格内容块ID
|
|
770
|
+
content_id = f"content_{cell_id}"
|
|
771
|
+
|
|
772
|
+
# 创建单元格块
|
|
773
|
+
cell_block = {
|
|
774
|
+
"block_id": cell_id,
|
|
775
|
+
"block_type": 32, # 表格单元格
|
|
776
|
+
"table_cell": {},
|
|
777
|
+
"children": [content_id]
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
# 创建单元格内容块
|
|
781
|
+
content_block = {
|
|
782
|
+
"block_id": content_id,
|
|
783
|
+
"block_type": 2, # 文本块
|
|
784
|
+
"text": {
|
|
785
|
+
"elements": [
|
|
786
|
+
{
|
|
787
|
+
"text_run": {
|
|
788
|
+
"content": str(cell_content) if cell_content else ""
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
],
|
|
792
|
+
"style": {
|
|
793
|
+
"bold": True,
|
|
794
|
+
"align": 2
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
"children": []
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
cell_blocks.append(cell_block)
|
|
801
|
+
cell_blocks.append(content_block)
|
|
802
|
+
|
|
803
|
+
cell_ids.extend(row_cells)
|
|
804
|
+
|
|
805
|
+
# 创建表格主块
|
|
806
|
+
table_block = {
|
|
807
|
+
"block_id": table_id,
|
|
808
|
+
"block_type": 31, # 表格
|
|
809
|
+
"table": {
|
|
810
|
+
"property": {
|
|
811
|
+
"row_size": rows,
|
|
812
|
+
"column_size": columns,
|
|
813
|
+
"header_row": True,
|
|
814
|
+
"column_width": column_width
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
"children": cell_ids
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
# 构建完整结构
|
|
821
|
+
result = {
|
|
822
|
+
"index": 0,
|
|
823
|
+
"children_id": [table_id],
|
|
824
|
+
"descendants": [table_block] + cell_blocks
|
|
825
|
+
}
|
|
826
|
+
# print(result)
|
|
827
|
+
return result
|
|
828
|
+
|
|
829
|
+
def build_bitable_text(self, text: str=None):
|
|
830
|
+
return {"title": text}
|
|
831
|
+
|
|
832
|
+
def build_block_image(self, file_path, percent: int=100, image_align: int=2):
|
|
833
|
+
|
|
834
|
+
from PIL import Image
|
|
835
|
+
with Image.open(file_path) as img:
|
|
836
|
+
width, height = img.size
|
|
837
|
+
image_width = int(width * percent / 100)
|
|
838
|
+
image_height = int(height * percent / 100)
|
|
839
|
+
|
|
840
|
+
return {
|
|
841
|
+
"index": 0,
|
|
842
|
+
"children": [
|
|
843
|
+
{
|
|
844
|
+
"block_type": 27,
|
|
845
|
+
"image": {}
|
|
846
|
+
}
|
|
847
|
+
],
|
|
848
|
+
"file_path": file_path,
|
|
849
|
+
"image_width": image_width,
|
|
850
|
+
"image_height": image_height,
|
|
851
|
+
"image_align": image_align
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
def upload_media(self, file_path: str, block_id: str):
|
|
855
|
+
file_size = os.path.getsize(file_path)
|
|
856
|
+
url = "https://open.feishu.cn/open-apis/drive/v1/medias/upload_all"
|
|
857
|
+
form = {'file_name': 'demo.jpeg',
|
|
858
|
+
'parent_type': 'docx_image',
|
|
859
|
+
'parent_node': block_id,
|
|
860
|
+
'size': str(file_size),
|
|
861
|
+
'file': (open(file_path, 'rb'))}
|
|
862
|
+
multi_form = MultipartEncoder(form)
|
|
863
|
+
headers = {
|
|
864
|
+
'Authorization': f'Bearer {self.parent.auth.fetch_token_from_file()}', ## 获取tenant_access_token, 需要替换为实际的token
|
|
865
|
+
}
|
|
866
|
+
headers['Content-Type'] = multi_form.content_type
|
|
867
|
+
response = requests.request("POST", url, headers=headers, data=multi_form)
|
|
868
|
+
return response.json()
|
|
869
|
+
|
|
870
|
+
def create_block(self, blocks, document_id):
|
|
871
|
+
# 交换blocks中元素的顺序
|
|
872
|
+
blocks.reverse()
|
|
873
|
+
|
|
874
|
+
for block in blocks:
|
|
875
|
+
time.sleep(1)
|
|
876
|
+
try:
|
|
877
|
+
if block['children'][0]['block_type'] != 27:
|
|
878
|
+
self.parent.docs.create_block(
|
|
879
|
+
document_id=document_id,
|
|
880
|
+
block_id=document_id,
|
|
881
|
+
payload=block
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
elif block['children'][0]['block_type'] == 27:
|
|
885
|
+
block_id = self.parent.docs.create_block(
|
|
886
|
+
document_id=document_id,
|
|
887
|
+
block_id=document_id,
|
|
888
|
+
payload=block
|
|
889
|
+
).data['children'][0]['block_id']
|
|
890
|
+
|
|
891
|
+
file_token = self.upload_media(
|
|
892
|
+
file_path=block['file_path'],
|
|
893
|
+
block_id=block_id
|
|
894
|
+
)['data']['file_token']
|
|
895
|
+
|
|
896
|
+
res = self.parent.docs.update_block(
|
|
897
|
+
document_id=document_id,
|
|
898
|
+
block_id=block_id,
|
|
899
|
+
replace_image_token=file_token,
|
|
900
|
+
image_width=block['image_width'],
|
|
901
|
+
image_height=block['image_height'],
|
|
902
|
+
image_align=block['image_align']
|
|
903
|
+
)
|
|
904
|
+
return res
|
|
905
|
+
except KeyError:
|
|
906
|
+
res = self.parent.docs.create_block(
|
|
907
|
+
document_id=document_id,
|
|
908
|
+
block_id=document_id,
|
|
909
|
+
payload=block
|
|
910
|
+
)
|
|
911
|
+
return res
|
|
912
|
+
except IndexError:
|
|
913
|
+
print(block)
|
|
914
|
+
|
|
915
|
+
def parse_bitable_data(self, fields, name):
|
|
916
|
+
final_data = None
|
|
917
|
+
|
|
918
|
+
if fields.get(name) != None:
|
|
919
|
+
if isinstance(fields[name], list):
|
|
920
|
+
if fields[name][0].get('type') == 'text':
|
|
921
|
+
final_data = fields[name][0].get('text')
|
|
922
|
+
elif fields[name][0].get('type') == 'url':
|
|
923
|
+
try:
|
|
924
|
+
text_2nd = fields[name][1].get('text')
|
|
925
|
+
final_data = fields[name][0].get('text') + text_2nd
|
|
926
|
+
except IndexError:
|
|
927
|
+
final_data = fields[name][0].get('text')
|
|
928
|
+
|
|
929
|
+
elif fields[name][0].get('type') == 1:
|
|
930
|
+
final_data = fields[name][0].get('value')[0]['text']
|
|
931
|
+
|
|
932
|
+
elif isinstance(fields[name], int):
|
|
933
|
+
# 将时间戳转换为时间字符串格式
|
|
934
|
+
final_data = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(fields[name] / 1000 ))
|
|
935
|
+
|
|
936
|
+
elif isinstance(fields[name], dict):
|
|
937
|
+
if fields[name].get('type') == 1:
|
|
938
|
+
final_data = fields[name].get('value')[0]['text']
|
|
939
|
+
elif fields[name].get('type') == 3:
|
|
940
|
+
final_data = fields[name].get('value')[0]
|
|
941
|
+
elif fields[name].get('type') == 5:
|
|
942
|
+
final_data = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(fields[name]['value'][0] / 1000 ))
|
|
943
|
+
else:
|
|
944
|
+
final_data = '待补充'
|
|
945
|
+
if isinstance(final_data, str):
|
|
946
|
+
return final_data
|
|
947
|
+
else:
|
|
948
|
+
return fields[name]
|
|
949
|
+
|
|
950
|
+
def get_user_info(self, email: str=None, mobile: str=None, get: Literal['open_id', 'all']='all') -> ReturnResponse:
|
|
951
|
+
payload = {
|
|
952
|
+
"include_resigned": True,
|
|
953
|
+
}
|
|
954
|
+
if email:
|
|
955
|
+
payload['emails'] = [email]
|
|
956
|
+
user_input = email
|
|
957
|
+
|
|
958
|
+
if mobile:
|
|
959
|
+
payload['mobiles'] = [mobile]
|
|
960
|
+
user_input = mobile
|
|
961
|
+
|
|
962
|
+
response = self.parent.request(path='/contact/v3/users/batch_get_id',
|
|
963
|
+
method='POST',
|
|
964
|
+
body=payload)
|
|
965
|
+
if response.code == 0:
|
|
966
|
+
if get == 'open_id':
|
|
967
|
+
return ReturnResponse(code=0, message=f'根据用户输入的 {user_input}, 获取用户信息成功', data=response.data['user_list'][0]['user_id'])
|
|
968
|
+
else:
|
|
969
|
+
return ReturnResponse(code=response.code, message=f"获取时失败, 报错请见 data 字段", data=response.data)
|
|
970
|
+
|
|
971
|
+
def format_rich_text(self, text: str, color: Literal['red', 'green', 'yellow', 'blue'], bold: bool=False):
|
|
972
|
+
if bold:
|
|
973
|
+
text = f"**{text}**"
|
|
974
|
+
|
|
975
|
+
if color:
|
|
976
|
+
text = f"<font color='{color}'>{text}</font>"
|
|
977
|
+
|
|
978
|
+
return text
|
|
979
|
+
|
|
980
|
+
def convert_str_to_dict(self, text: str):
|
|
981
|
+
return json.loads(text)
|
|
982
|
+
|
|
983
|
+
def parse_message_card_elements(self, elements: list | dict) -> str:
|
|
984
|
+
"""
|
|
985
|
+
递归解析飞书消息卡片 elements,收集所有 tag 为 'text' 的文本并拼接返回。
|
|
986
|
+
|
|
987
|
+
此方法兼容以下结构:
|
|
988
|
+
- 二维列表:例如 [[{...}, {...}]]
|
|
989
|
+
- 多层嵌套:字典中包含 'elements'、'content'、'children' 等容器键
|
|
990
|
+
- 忽略未知/非 text 标签,例如 'unknown'
|
|
991
|
+
|
|
992
|
+
Args:
|
|
993
|
+
elements (list | dict): 飞书消息卡片的 elements 字段,可能是列表或字典。
|
|
994
|
+
|
|
995
|
+
Returns:
|
|
996
|
+
str: 拼接后的文本内容。
|
|
997
|
+
"""
|
|
998
|
+
|
|
999
|
+
texts: list[str] = []
|
|
1000
|
+
|
|
1001
|
+
def walk(node: Any) -> None:
|
|
1002
|
+
if node is None:
|
|
1003
|
+
return
|
|
1004
|
+
if isinstance(node, dict):
|
|
1005
|
+
tag = node.get('tag')
|
|
1006
|
+
if tag == 'text' and isinstance(node.get('text'), str):
|
|
1007
|
+
texts.append(node['text'])
|
|
1008
|
+
# 递归遍历常见的容器键
|
|
1009
|
+
for key in ('elements', 'content', 'children'):
|
|
1010
|
+
value = node.get(key)
|
|
1011
|
+
if isinstance(value, (list, tuple, dict)):
|
|
1012
|
+
walk(value)
|
|
1013
|
+
elif isinstance(node, (list, tuple)):
|
|
1014
|
+
for item in node:
|
|
1015
|
+
walk(item)
|
|
1016
|
+
|
|
1017
|
+
walk(elements)
|
|
1018
|
+
return ''.join(texts)
|
|
1019
|
+
|
|
1020
|
+
def send_message_notify(self,
|
|
1021
|
+
receive_id: str='ou_ca3fc788570865cbbf59bfff43621a78',
|
|
1022
|
+
color: Literal['red', 'green', 'blue']='red',
|
|
1023
|
+
title: str='Test',
|
|
1024
|
+
sub_title: str='未填写子标题',
|
|
1025
|
+
priority: str='P0',
|
|
1026
|
+
content: str='Test'
|
|
1027
|
+
):
|
|
1028
|
+
return self.parent.message.send_card(
|
|
1029
|
+
template_id="AAqzcy5Qrx84H",
|
|
1030
|
+
template_variable={
|
|
1031
|
+
"color": color,
|
|
1032
|
+
"title": title,
|
|
1033
|
+
"sub_title": sub_title,
|
|
1034
|
+
"priority": priority,
|
|
1035
|
+
"content": content
|
|
1036
|
+
},
|
|
1037
|
+
receive_id=receive_id
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
def get_user_info_by_open_id(self, open_id: str, get: Literal['name', 'all']='all'):
|
|
1041
|
+
response = self.parent.request(path=f'/contact/v3/users/{open_id}?department_id_type=open_department_id&user_id_type=open_id',
|
|
1042
|
+
method='GET')
|
|
1043
|
+
if response.code == 0:
|
|
1044
|
+
if get == 'name':
|
|
1045
|
+
return response.data['user']['name']
|
|
1046
|
+
else:
|
|
1047
|
+
return response.data
|
|
1048
|
+
else:
|
|
1049
|
+
return None
|