166 lines
5.6 KiB
Python
166 lines
5.6 KiB
Python
![]() |
from flask import Flask, request, jsonify
|
|||
|
import os, base64, hashlib, time, requests, json
|
|||
|
|
|||
|
app = Flask(__name__)
|
|||
|
|
|||
|
APP_ID = None
|
|||
|
APP_SECRET = None
|
|||
|
REDIRECT_URI = None
|
|||
|
code_verifier_store = {}
|
|||
|
token_data = {}
|
|||
|
|
|||
|
# 生成 code_verifier / code_challenge
|
|||
|
def generate_code_verifier():
|
|||
|
return base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode('utf-8')
|
|||
|
|
|||
|
def generate_code_challenge(code_verifier):
|
|||
|
digest = hashlib.sha256(code_verifier.encode('utf-8')).digest()
|
|||
|
return base64.urlsafe_b64encode(digest).rstrip(b'=').decode('utf-8')
|
|||
|
|
|||
|
def save_token(app_id, token):
|
|||
|
with open(f"token_{app_id}.json", "w", encoding="utf-8") as f:
|
|||
|
json.dump(token, f)
|
|||
|
|
|||
|
def load_token(app_id):
|
|||
|
path = f"token_{app_id}.json"
|
|||
|
if os.path.exists(path):
|
|||
|
with open(path, "r", encoding="utf-8") as f:
|
|||
|
return json.load(f)
|
|||
|
return None
|
|||
|
|
|||
|
def ensure_token_valid():
|
|||
|
global token_data
|
|||
|
if not token_data:
|
|||
|
return False
|
|||
|
if time.time() >= token_data.get("expires_at", 0):
|
|||
|
# refresh token
|
|||
|
resp = requests.post(
|
|||
|
"https://open.feishu.cn/open-apis/authen/v2/oauth/token",
|
|||
|
json={
|
|||
|
"grant_type": "refresh_token",
|
|||
|
"client_id": APP_ID,
|
|||
|
"client_secret": APP_SECRET,
|
|||
|
"refresh_token": token_data["refresh_token"]
|
|||
|
}
|
|||
|
)
|
|||
|
data = resp.json()
|
|||
|
if data.get("code") == 0:
|
|||
|
token_data["access_token"] = data["access_token"]
|
|||
|
token_data["refresh_token"] = data["refresh_token"]
|
|||
|
token_data["expires_at"] = time.time() + data["expires_in"] - 60
|
|||
|
save_token(APP_ID, token_data)
|
|||
|
return True
|
|||
|
return False
|
|||
|
return True
|
|||
|
|
|||
|
# 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)
|