diff --git a/main.py b/main.py index 9abeb59..c4d0991 100755 --- a/main.py +++ b/main.py @@ -199,6 +199,12 @@ if __name__ == "__main__": ) sys.exit() + # Kiểm tra access token trước khi chạy (chỉ cho Threads mode) + if not args.reddit: + from utils.check_token import preflight_check + + preflight_check() + try: if args.mode == "scheduled": # Chế độ lên lịch tự động diff --git a/scheduler/pipeline.py b/scheduler/pipeline.py index ef520f8..cf0b696 100644 --- a/scheduler/pipeline.py +++ b/scheduler/pipeline.py @@ -50,6 +50,11 @@ def run_pipeline(post_id: Optional[str] = None) -> Optional[str]: print_step("🚀 Bắt đầu pipeline tạo video...") + # Preflight: kiểm tra access token trước khi gọi API + from utils.check_token import preflight_check + + preflight_check() + try: # Step 1: Lấy nội dung từ Threads print_step("📱 Bước 1: Lấy nội dung từ Threads...") diff --git a/utils/check_token.py b/utils/check_token.py new file mode 100644 index 0000000..a23d8f3 --- /dev/null +++ b/utils/check_token.py @@ -0,0 +1,198 @@ +""" +Preflight Access-Token Checker — chạy trước khi pipeline bắt đầu. + +Kiểm tra: +1. access_token có được cấu hình trong config.toml không. +2. Token có hợp lệ trên Threads API (/me endpoint) không. +3. Nếu token hết hạn → tự động thử refresh. +4. user_id trong config khớp với user sở hữu token không. + +Usage: + # Gọi trực tiếp: + python -m utils.check_token + + # Hoặc import trong code: + from utils.check_token import preflight_check + preflight_check() # raises SystemExit on failure +""" + +import sys +from typing import Optional + +import requests + +from utils import settings +from utils.console import print_step, print_substep + +THREADS_API_BASE = "https://graph.threads.net/v1.0" +_REQUEST_TIMEOUT = 15 # seconds – preflight should be fast + + +class TokenCheckError(Exception): + """Raised when the access-token preflight fails.""" + + +def _call_me_endpoint(access_token: str) -> dict: + """GET /me?fields=id,username&access_token=… with minimal retry.""" + url = f"{THREADS_API_BASE}/me" + params = { + "fields": "id,username", + "access_token": access_token, + } + response = requests.get(url, params=params, timeout=_REQUEST_TIMEOUT) + + # HTTP-level errors + if response.status_code == 401: + raise TokenCheckError( + "Access token không hợp lệ hoặc đã hết hạn (HTTP 401).\n" + "→ Cập nhật [threads.creds] access_token trong config.toml." + ) + if response.status_code == 403: + raise TokenCheckError( + "Token thiếu quyền (HTTP 403).\n" + "→ Đảm bảo token có quyền threads_basic_read trong Meta Developer Portal." + ) + response.raise_for_status() + + data = response.json() + + # Graph API may return 200 with an error body + if "error" in data: + err = data["error"] + msg = err.get("message", "Unknown error") + code = err.get("code", 0) + raise TokenCheckError(f"Threads API trả về lỗi: {msg} (code={code})") + + return data + + +def _try_refresh(access_token: str) -> Optional[str]: + """Attempt to refresh a long-lived Threads token. + + Returns new token string, or None if refresh is not possible. + """ + url = f"{THREADS_API_BASE}/refresh_access_token" + try: + resp = requests.get( + url, + params={ + "grant_type": "th_refresh_token", + "access_token": access_token, + }, + timeout=_REQUEST_TIMEOUT, + ) + resp.raise_for_status() + data = resp.json() + if "error" in data: + return None + return data.get("access_token") or None + except requests.RequestException: + return None + + +def preflight_check() -> None: + """Validate the Threads access token configured in *config.toml*. + + On success, prints a confirmation and returns normally. + On failure, prints actionable diagnostics and raises ``SystemExit(1)``. + """ + print_step("🔑 Kiểm tra access token trước khi chạy...") + + # --- 1. Check config values exist ----------------------------------- + try: + threads_creds = settings.config["threads"]["creds"] + access_token: str = threads_creds.get("access_token", "").strip() + user_id: str = threads_creds.get("user_id", "").strip() + except (KeyError, TypeError): + print_substep( + "❌ Thiếu cấu hình [threads.creds] trong config.toml.\n" + " Cần có access_token và user_id.", + style="bold red", + ) + sys.exit(1) + + if not access_token: + print_substep( + "❌ access_token trống trong config.toml!\n" + " Lấy token tại: https://developers.facebook.com/docs/threads/get-started", + style="bold red", + ) + sys.exit(1) + + if not user_id: + print_substep( + "❌ user_id trống trong config.toml!\n" + " Lấy user_id bằng cách gọi /me với access token.", + style="bold red", + ) + sys.exit(1) + + # --- 2. Validate token via /me endpoint ----------------------------- + try: + me_data = _call_me_endpoint(access_token) + except TokenCheckError as exc: + # Token invalid → try refresh + print_substep( + f"⚠️ Token hiện tại không hợp lệ: {exc}\n" " Đang thử refresh token...", + style="bold yellow", + ) + new_token = _try_refresh(access_token) + if new_token: + try: + me_data = _call_me_endpoint(new_token) + access_token = new_token + # Update in-memory config so downstream code uses the new token + settings.config["threads"]["creds"]["access_token"] = new_token + print_substep("✅ Token đã được refresh thành công!", style="bold green") + except TokenCheckError as inner: + print_substep( + f"❌ Token mới sau refresh vẫn lỗi: {inner}\n" + " Vui lòng lấy token mới từ Meta Developer Portal:\n" + " https://developers.facebook.com/docs/threads/get-started", + style="bold red", + ) + sys.exit(1) + else: + print_substep( + "❌ Không thể refresh token.\n" + " Vui lòng lấy token mới từ Meta Developer Portal:\n" + " https://developers.facebook.com/docs/threads/get-started", + style="bold red", + ) + sys.exit(1) + except requests.RequestException as exc: + print_substep( + f"❌ Lỗi kết nối khi kiểm tra token: {exc}\n" " Kiểm tra kết nối mạng và thử lại.", + style="bold red", + ) + sys.exit(1) + + # --- 3. Cross-check user_id ---------------------------------------- + api_user_id = me_data.get("id", "") + api_username = me_data.get("username", "N/A") + + if api_user_id and api_user_id != user_id: + print_substep( + f"⚠️ user_id trong config ({user_id}) khác với user sở hữu token ({api_user_id}).\n" + " Nếu bạn muốn lấy threads của chính mình, hãy cập nhật user_id trong config.toml.\n" + " Đang tiếp tục với token hiện tại...", + style="bold yellow", + ) + + print_substep( + f"✅ Access token hợp lệ — @{api_username} (ID: {api_user_id})", + style="bold green", + ) + + +# Allow running standalone: python -m utils.check_token +if __name__ == "__main__": + from pathlib import Path + + directory = Path().absolute() + settings.check_toml( + f"{directory}/utils/.config.template.toml", + f"{directory}/config.toml", + ) + preflight_check() + print_step("🎉 Tất cả kiểm tra đều OK — sẵn sàng chạy!")