200 lines
6.5 KiB
Python
200 lines
6.5 KiB
Python
import os
|
||
import time
|
||
import json
|
||
from flask import Flask, request, jsonify, redirect
|
||
import requests
|
||
import base64
|
||
import hashlib
|
||
from OaConfig import TOKEN_STORE_FILE
|
||
app = Flask(__name__)
|
||
# 全局变量缓存token和配置
|
||
token_data = {}
|
||
APP_ID = None
|
||
APP_SECRET = None
|
||
REDIRECT_URI = None
|
||
code_verifier = None
|
||
|
||
|
||
def save_token(app_id, data):
|
||
store = {}
|
||
if os.path.exists(TOKEN_STORE_FILE):
|
||
with open(TOKEN_STORE_FILE, "r") as f:
|
||
store = json.load(f)
|
||
store[app_id] = data
|
||
with open(TOKEN_STORE_FILE, "w") as f:
|
||
json.dump(store, f)
|
||
|
||
|
||
def load_token(app_id):
|
||
if os.path.exists(TOKEN_STORE_FILE):
|
||
with open(TOKEN_STORE_FILE, "r") as f:
|
||
store = json.load(f)
|
||
return store.get(app_id, {})
|
||
return {}
|
||
|
||
|
||
def ensure_token_valid():
|
||
global token_data
|
||
now = time.time()
|
||
if not token_data or not token_data.get("access_token") or now > token_data.get("expires_at", 0):
|
||
print("[ensure_token_valid] Access token expired or missing, try refresh token...")
|
||
if not refresh_access_token():
|
||
print("[ensure_token_valid] Refresh token failed, need user authorization")
|
||
return False
|
||
return True
|
||
|
||
|
||
def refresh_access_token():
|
||
global token_data, APP_ID, APP_SECRET
|
||
if not token_data.get("refresh_token"):
|
||
print("[refresh_access_token] No refresh_token available")
|
||
return False
|
||
|
||
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()
|
||
print(f"[refresh_access_token] Response: {data}")
|
||
|
||
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)
|
||
print("[refresh_access_token] Refresh success")
|
||
return True
|
||
else:
|
||
print("[refresh_access_token] Refresh failed")
|
||
return False
|
||
|
||
|
||
def generate_code_verifier():
|
||
# 生成随机字符串作为 code_verifier
|
||
return base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode('utf-8')
|
||
|
||
def generate_code_challenge(code_verifier):
|
||
# 对 code_verifier 做 sha256,再 base64 urlsafe 编码得到 code_challenge
|
||
sha256 = hashlib.sha256(code_verifier.encode('utf-8')).digest()
|
||
return base64.urlsafe_b64encode(sha256).rstrip(b'=').decode('utf-8')
|
||
|
||
@app.route("/pkce", methods=["GET"])
|
||
def get_pkce():
|
||
code_verifier = generate_code_verifier()
|
||
code_challenge = generate_code_challenge(code_verifier)
|
||
return jsonify({
|
||
"code_verifier": code_verifier,
|
||
"code_challenge": code_challenge
|
||
})
|
||
|
||
@app.route("/oauth/callback")
|
||
def oauth_callback():
|
||
global token_data, APP_ID, APP_SECRET, REDIRECT_URI, code_verifier
|
||
|
||
code = request.args.get("code")
|
||
state = request.args.get("state")
|
||
|
||
if not code:
|
||
return jsonify({"error": "Missing code param"}), 400
|
||
if not all([APP_ID, APP_SECRET, REDIRECT_URI, code_verifier]):
|
||
return jsonify({"error": "Missing config parameters"}), 400
|
||
|
||
# 用code换token
|
||
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": code_verifier
|
||
}
|
||
)
|
||
data = resp.json()
|
||
print(f"[oauth_callback] token response: {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
|
||
|
||
|
||
@app.route("/set_config", methods=["POST"])
|
||
def set_config():
|
||
global APP_ID, APP_SECRET, REDIRECT_URI, code_verifier, 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")
|
||
code_verifier = data.get("code_verifier")
|
||
|
||
if not all([APP_ID, APP_SECRET, REDIRECT_URI, code_verifier]):
|
||
return jsonify({"error": "缺少必要参数 APP_ID, APP_SECRET, REDIRECT_URI 或 code_verifier"}), 400
|
||
|
||
# 读取之前保存的token
|
||
token_data = load_token(APP_ID)
|
||
return jsonify({"message": "配置成功", "token_data": token_data})
|
||
|
||
|
||
@app.route("/calendar_events", methods=["POST"])
|
||
def calendar_events():
|
||
global APP_ID, APP_SECRET, token_data
|
||
if not all([APP_ID, APP_SECRET]):
|
||
return jsonify({"error": "请先调用 /set_config 设置参数"}), 400
|
||
|
||
if not ensure_token_valid():
|
||
# 引导用户去授权页面
|
||
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 calendar:calendar:readonly offline_access"
|
||
f"&state=auth"
|
||
f"&code_challenge=YOUR_CODE_CHALLENGE" # 需要用你的code_challenge替换
|
||
f"&code_challenge_method=S256"
|
||
)
|
||
return jsonify({
|
||
"message": "请先授权",
|
||
"auth_url": auth_url
|
||
}), 401
|
||
|
||
headers = {
|
||
"Authorization": f"Bearer {token_data['access_token']}"
|
||
}
|
||
resp = requests.get("https://open.feishu.cn/open-apis/calendar/v4/calendars", headers=headers)
|
||
data = resp.json()
|
||
if data.get("code") != 0:
|
||
return jsonify({"error": "获取日历列表失败", "raw": data}), 500
|
||
|
||
calendars = data.get("data", {}).get("calendar_list", [])
|
||
events_all = []
|
||
for calendar in calendars:
|
||
calendar_id = calendar["calendar_id"]
|
||
ev_resp = requests.get(
|
||
f"https://open.feishu.cn/open-apis/calendar/v4/calendars/{calendar_id}/events",
|
||
headers=headers
|
||
)
|
||
ev_data = ev_resp.json()
|
||
if ev_data.get("code") != 0:
|
||
continue
|
||
events = ev_data.get("data", {}).get("items", [])
|
||
events_all.extend(events)
|
||
|
||
return jsonify({"events": events_all})
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app.run(port=8888, debug=True)
|