|
|
|
|
@ -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!")
|