sdk/oapi-sdk-python-2_main/main.py

299 lines
10 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import csv
import io
from flask import Flask, jsonify, redirect, request
import time
import requests
from lark_oapi import Client, LogLevel, logger
from lark_oapi.api.drive.v1 import ListFileRequest
import chardet
from flask import Flask, request, jsonify
import lark_oapi as lark
from attendance import get_user_ids_and_names,get_attendance_data,save_to_csv,get_all_department_ids
from message import fetch_messages_for_chat,fetch_chats,save_messages,str_to_timestamp_seconds
from bitable import read_csv_with_auto_encoding_from_bytes,get_table_ids,extract_app_token,get_all_records
from files import list_files_recursive,extract_folder_token_from_url
from calendars import generate_code_verifier,generate_code_challenge,save_token,ensure_token_valid,load_token
app = Flask(__name__)
TOKEN_STORE_FILE = "token_store.json"
user_access_token = None
token_expires_at = 0
APP_ID = None
APP_SECRET = None
REDIRECT_URI = None
#获取聊天消息
@app.route("/fetch_messages", methods=["POST"])
def fetch_messages_api():
data = request.json
if not data:
return jsonify({"error": "请求体必须是 JSON 格式"}), 400
app_id = data.get("app_id")
app_secret = data.get("app_secret")
if not app_id or not app_secret:
return jsonify({"error": "必须提供 app_id 和 app_secret"}), 400
start_time_str = data.get("start_time")
end_time_str = data.get("end_time")
if not start_time_str or not end_time_str:
return jsonify({"error": "必须提供 start_time 和 end_time格式YYYY-MM-DD HH:MM:SS"}), 400
try:
start_time = str_to_timestamp_seconds(start_time_str)
end_time = str_to_timestamp_seconds(end_time_str)
except ValueError as e:
return jsonify({"error": str(e)}), 400
if start_time > end_time:
return jsonify({"error": "start_time 不能晚于 end_time"}), 400
client = lark.Client.builder() \
.app_id(app_id) \
.app_secret(app_secret) \
.log_level(lark.LogLevel.ERROR) \
.build()
chats = fetch_chats(client)
if not chats:
return jsonify({"error": "获取群聊失败"}), 500
all_messages = []
for chat in chats:
msgs = fetch_messages_for_chat(client, chat.chat_id, chat.name, start_time, end_time)
all_messages.extend(msgs)
if all_messages:
save_messages(all_messages)
return jsonify({"msg": f"已保存消息,共计 {len(all_messages)}"}), 200
else:
return jsonify({"msg": "该时间区间内无消息"}), 200
#获取文档
@app.route("/list_folder", methods=["POST"])
def list_folder():
app_id, app_secret, csv_file = request.form.get("app_id"), request.form.get("app_secret"), request.files.get("csv_file")
if not all([app_id, app_secret, csv_file]): return jsonify({"error": "缺少参数 app_id, app_secret 或 csv_file"}), 400
client = lark.Client.builder().app_id(app_id).app_secret(app_secret).log_level(lark.LogLevel.ERROR).build()
option = lark.RequestOption.builder().build()
raw_bytes = csv_file.stream.read()
encoding = chardet.detect(raw_bytes)["encoding"] or "utf-8"
reader = csv.reader(io.StringIO(raw_bytes.decode(encoding)))
tree_result = []
next(reader, None) # Skip header
for row in reader:
if len(row) < 2: continue
folder_token = extract_folder_token_from_url(row[1].strip())
if folder_token: list_files_recursive(client, folder_token, option, tree_result)
return jsonify(tree_result)
#获取打卡记录
@app.route('/attendance', methods=['POST'])
def attendance():
data = request.get_json()
app_id = data.get("app_id")
app_secret = data.get("app_secret")
start_time_str = data.get("start_time")
end_time_str = data.get("end_time")
if not all([app_id, app_secret, start_time_str, end_time_str]):
return jsonify({"error": "缺少必要的参数: app_id, app_secret, start_time, end_time"}), 400
start_time = str_to_timestamp_seconds(start_time_str)
end_time = str_to_timestamp_seconds(end_time_str)
client = lark.Client.builder() \
.app_id(app_id) \
.app_secret(app_secret) \
.log_level(lark.LogLevel.ERROR) \
.build()
# 获取所有部门 ID
department_ids = get_all_department_ids(client)
print(f"共获取到 {len(department_ids)} 个部门")
for idx, dep_id in enumerate(department_ids, start=1):
print(f"{idx}. {dep_id}")
all_check_in_records = []
for dep_id in department_ids:
user_ids, user_names = get_user_ids_and_names(client, dep_id)
print(f"\n部门 {dep_id} 员工列表(共 {len(user_ids)} 人):")
for uid in user_ids:
print(f" - {uid} : {user_names.get(uid, '未知姓名')}")
dep_records = get_attendance_data(client, user_ids, start_time, end_time, user_names)
all_check_in_records.extend(dep_records)
structured_data, file_path = save_to_csv(all_check_in_records)
return jsonify({
"message": "打卡统计已保存",
"file_path": file_path,
"data": structured_data
}), 200
#获取多维表格数据
@app.route('/fetch_records', methods=['POST'])
def fetch_records():
try:
APP_ID = request.form.get("app_id")
APP_SECRET = request.form.get("app_secret")
file = request.files.get("file")
if not all([APP_ID, APP_SECRET, file]):
return jsonify({"error": "缺少参数 app_id, app_secret 或 上传的文件"}), 400
csv_bytes = file.read()
df = read_csv_with_auto_encoding_from_bytes(csv_bytes)
client = Client.builder().app_id(APP_ID).app_secret(APP_SECRET).log_level(LogLevel.INFO).build()
results = []
for _, row in df.iterrows():
url = row.get("url", "")
app_token = extract_app_token(url)
if not app_token:
continue
table_ids = get_table_ids(client, app_token)
if not table_ids:
continue
tables = []
for table_id in table_ids:
items = get_all_records(client, app_token, table_id)
tables.append({
"table_id": table_id,
"items": items
})
results.append({
"app_token": app_token,
"tables": tables
})
return jsonify(results)
except Exception as e:
return jsonify({"error": str(e)}), 500
#获取日历日程安排
token_data = {}
# 1⃣ 生成 PKCE 参数
@app.route("/pkce", methods=["GET"])
def get_pkce():
global code_verifier_store
code_verifier = generate_code_verifier()
code_challenge = generate_code_challenge(code_verifier)
# 保存,方便 callback 时使用
code_verifier_store["verifier"] = code_verifier
code_verifier_store["challenge"] = code_challenge
return jsonify({
"code_verifier": code_verifier,
"code_challenge": code_challenge
})
# 2⃣ 回调换取 token
@app.route("/oauth/callback")
def oauth_callback():
global token_data
code = request.args.get("code")
if not code:
return jsonify({"error": "Missing code"}), 400
if not all([APP_ID, APP_SECRET, REDIRECT_URI]):
return jsonify({"error": "Missing config"}), 400
verifier = code_verifier_store.get("verifier")
if not verifier:
return jsonify({"error": "Missing code_verifier"}), 400
resp = requests.post(
"https://open.feishu.cn/open-apis/authen/v2/oauth/token",
json={
"grant_type": "authorization_code",
"client_id": APP_ID,
"client_secret": APP_SECRET,
"code": code,
"redirect_uri": REDIRECT_URI,
"code_verifier": verifier
}
)
data = resp.json()
print("[callback] token resp:", data)
if data.get("code") == 0:
token_data = {
"access_token": data["access_token"],
"refresh_token": data["refresh_token"],
"expires_at": time.time() + data["expires_in"] - 60
}
save_token(APP_ID, token_data)
return "授权成功,可以回到终端调用接口了。"
else:
return jsonify({"error": "Token获取失败", "raw": data}), 400
# 3⃣ 设置配置参数
@app.route("/set_config", methods=["POST"])
def set_config():
global APP_ID, APP_SECRET, REDIRECT_URI, token_data
data = request.get_json(force=True)
APP_ID = data.get("APP_ID")
APP_SECRET = data.get("APP_SECRET")
REDIRECT_URI = data.get("REDIRECT_URI")
if not all([APP_ID, APP_SECRET, REDIRECT_URI]):
return jsonify({"error": "缺少 APP_ID, APP_SECRET, REDIRECT_URI"}), 400
token_data = load_token(APP_ID)
return jsonify({"message": "配置成功", "token_data": token_data})
# 4⃣ 获取日历和日程
@app.route("/calendar_events", methods=["POST"])
def calendar_events():
global token_data
if not ensure_token_valid():
challenge = code_verifier_store.get("challenge")
if not challenge:
return jsonify({"error": "请先访问 /pkce 获取 PKCE 参数"}), 400
auth_url = (
f"https://accounts.feishu.cn/open-apis/authen/v1/authorize"
f"?client_id={APP_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&scope=calendar:calendar.event:read%20calendar:calendar:readonly%20offline_access"
f"&state=auth"
f"&response_type=code"
f"&code_challenge={challenge}"
f"&code_challenge_method=S256"
)
return jsonify({"message": "需要授权", "auth_url": auth_url}), 401
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
calendars_resp = requests.get("https://open.feishu.cn/open-apis/calendar/v4/calendars", headers=headers).json()
if calendars_resp.get("code") != 0:
return jsonify({"error": "获取日历列表失败", "raw": calendars_resp}), 500
calendars = calendars_resp.get("data", {}).get("calendar_list", [])
all_events = []
for c in calendars:
cid = c["calendar_id"]
ev_resp = requests.get(
f"https://open.feishu.cn/open-apis/calendar/v4/calendars/{cid}/events",
headers=headers
).json()
if ev_resp.get("code") == 0:
all_events.extend(ev_resp.get("data", {}).get("items", []))
return jsonify({"events": all_events})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8888 ,debug=True)