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)