Skip to content

Commit 36b702e

Browse files
Merge pull request #2 from thaitien280401-stack/copilot/fix-no-threads-error
Preflight access token validation before pipeline execution
2 parents c00db83 + 5975e65 commit 36b702e

4 files changed

Lines changed: 428 additions & 18 deletions

File tree

‎main.py‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@ def parse_args():
199199
)
200200
sys.exit()
201201

202+
# Kiểm tra access token trước khi chạy (chỉ cho Threads mode)
203+
if not args.reddit:
204+
from utils.check_token import preflight_check
205+
206+
preflight_check()
207+
202208
try:
203209
if args.mode == "scheduled":
204210
# Chế độ lên lịch tự động
@@ -285,10 +291,25 @@ def parse_args():
285291
except (KeyError, TypeError):
286292
pass
287293

288-
print_step(
289-
f"Đã xảy ra lỗi! Vui lòng thử lại hoặc báo lỗi trên GitHub.\n"
290-
f"Phiên bản: {__VERSION__}\n"
291-
f"Lỗi: {err}\n"
292-
f'Config: {config.get("settings", {})}'
293-
)
294+
# Import here to avoid circular import at module level
295+
from threads.threads_client import ThreadsAPIError
296+
297+
if isinstance(err, ThreadsAPIError):
298+
print_step(
299+
f"❌ Lỗi xác thực Threads API!\n"
300+
f"Phiên bản: {__VERSION__}\n"
301+
f"Lỗi: {err}\n\n"
302+
"Hướng dẫn khắc phục:\n"
303+
"1. Kiểm tra access_token trong config.toml còn hiệu lực không\n"
304+
"2. Lấy token mới tại: https://developers.facebook.com/docs/threads/get-started\n"
305+
"3. Đảm bảo token có quyền: threads_basic_read\n"
306+
"4. Kiểm tra user_id khớp với tài khoản Threads"
307+
)
308+
else:
309+
print_step(
310+
f"Đã xảy ra lỗi! Vui lòng thử lại hoặc báo lỗi trên GitHub.\n"
311+
f"Phiên bản: {__VERSION__}\n"
312+
f"Lỗi: {err}\n"
313+
f'Config: {config.get("settings", {})}'
314+
)
294315
raise err

‎scheduler/pipeline.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ def run_pipeline(post_id: Optional[str] = None) -> Optional[str]:
5050

5151
print_step("🚀 Bắt đầu pipeline tạo video...")
5252

53+
# Preflight: kiểm tra access token trước khi gọi API
54+
from utils.check_token import preflight_check
55+
56+
preflight_check()
57+
5358
try:
5459
# Step 1: Lấy nội dung từ Threads
5560
print_step("📱 Bước 1: Lấy nội dung từ Threads...")

‎threads/threads_client.py‎

Lines changed: 198 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77

88
import re
9+
import time as _time
910
from typing import Dict, List, Optional
1011

1112
import requests
@@ -18,6 +19,20 @@
1819

1920
THREADS_API_BASE = "https://graph.threads.net/v1.0"
2021

22+
# Retry configuration for transient failures
23+
_MAX_RETRIES = 3
24+
_RETRY_DELAY_SECONDS = 2
25+
_REQUEST_TIMEOUT_SECONDS = 30
26+
27+
28+
class ThreadsAPIError(Exception):
29+
"""Lỗi khi gọi Threads API (token hết hạn, quyền thiếu, v.v.)."""
30+
31+
def __init__(self, message: str, error_type: str = "", error_code: int = 0):
32+
self.error_type = error_type
33+
self.error_code = error_code
34+
super().__init__(message)
35+
2136

2237
class ThreadsClient:
2338
"""Client để tương tác với Threads API (Meta)."""
@@ -26,21 +41,131 @@ def __init__(self):
2641
self.access_token = settings.config["threads"]["creds"]["access_token"]
2742
self.user_id = settings.config["threads"]["creds"]["user_id"]
2843
self.session = requests.Session()
29-
self.session.headers.update(
30-
{
31-
"Authorization": f"Bearer {self.access_token}",
32-
}
33-
)
3444

3545
def _get(self, endpoint: str, params: Optional[dict] = None) -> dict:
36-
"""Make a GET request to the Threads API."""
46+
"""Make a GET request to the Threads API with retry logic.
47+
48+
Raises:
49+
ThreadsAPIError: If the API returns an error in the response body.
50+
requests.HTTPError: If the HTTP request fails after retries.
51+
"""
3752
url = f"{THREADS_API_BASE}/{endpoint}"
3853
if params is None:
3954
params = {}
4055
params["access_token"] = self.access_token
41-
response = self.session.get(url, params=params)
42-
response.raise_for_status()
43-
return response.json()
56+
57+
last_exception: Optional[Exception] = None
58+
for attempt in range(1, _MAX_RETRIES + 1):
59+
try:
60+
response = self.session.get(url, params=params, timeout=_REQUEST_TIMEOUT_SECONDS)
61+
62+
# Check for HTTP-level errors with detailed messages
63+
if response.status_code == 401:
64+
raise ThreadsAPIError(
65+
"Access token không hợp lệ hoặc đã hết hạn (HTTP 401). "
66+
"Vui lòng cập nhật access_token trong config.toml.",
67+
error_type="OAuthException",
68+
error_code=401,
69+
)
70+
if response.status_code == 403:
71+
raise ThreadsAPIError(
72+
"Không có quyền truy cập (HTTP 403). Kiểm tra quyền "
73+
"threads_basic_read trong Meta Developer Portal.",
74+
error_type="PermissionError",
75+
error_code=403,
76+
)
77+
response.raise_for_status()
78+
data = response.json()
79+
80+
# Meta Graph API có thể trả về 200 nhưng body chứa error
81+
if "error" in data:
82+
err = data["error"]
83+
error_msg = err.get("message", "Unknown API error")
84+
error_type = err.get("type", "")
85+
error_code = err.get("code", 0)
86+
raise ThreadsAPIError(
87+
f"Threads API error: {error_msg} (type={error_type}, code={error_code})",
88+
error_type=error_type,
89+
error_code=error_code,
90+
)
91+
92+
return data
93+
94+
except (requests.ConnectionError, requests.Timeout) as exc:
95+
last_exception = exc
96+
if attempt < _MAX_RETRIES:
97+
print_substep(
98+
f"Lỗi kết nối (lần {attempt}/{_MAX_RETRIES}), "
99+
f"thử lại sau {_RETRY_DELAY_SECONDS}s...",
100+
style="bold yellow",
101+
)
102+
_time.sleep(_RETRY_DELAY_SECONDS)
103+
continue
104+
raise
105+
except (ThreadsAPIError, requests.HTTPError):
106+
raise
107+
108+
def validate_token(self) -> dict:
109+
"""Kiểm tra access token có hợp lệ bằng cách gọi /me endpoint.
110+
111+
Returns:
112+
User profile data nếu token hợp lệ.
113+
114+
Raises:
115+
ThreadsAPIError: Nếu token không hợp lệ hoặc đã hết hạn.
116+
"""
117+
try:
118+
return self._get("me", params={"fields": "id,username"})
119+
except (ThreadsAPIError, requests.HTTPError) as exc:
120+
raise ThreadsAPIError(
121+
"Access token không hợp lệ hoặc đã hết hạn. "
122+
"Vui lòng cập nhật access_token trong config.toml. "
123+
"Hướng dẫn: https://developers.facebook.com/docs/threads/get-started\n"
124+
f"Chi tiết: {exc}"
125+
) from exc
126+
127+
def refresh_token(self) -> str:
128+
"""Làm mới access token (long-lived token).
129+
130+
Meta Threads API cho phép refresh long-lived tokens.
131+
Endpoint: GET /refresh_access_token?grant_type=th_refresh_token&access_token=...
132+
133+
Returns:
134+
Access token mới.
135+
136+
Raises:
137+
ThreadsAPIError: Nếu không thể refresh token.
138+
"""
139+
try:
140+
url = f"{THREADS_API_BASE}/refresh_access_token"
141+
response = self.session.get(
142+
url,
143+
params={
144+
"grant_type": "th_refresh_token",
145+
"access_token": self.access_token,
146+
},
147+
timeout=_REQUEST_TIMEOUT_SECONDS,
148+
)
149+
response.raise_for_status()
150+
data = response.json()
151+
152+
if "error" in data:
153+
error_msg = data["error"].get("message", "Unknown error")
154+
raise ThreadsAPIError(f"Không thể refresh token: {error_msg}")
155+
156+
new_token = data.get("access_token", "")
157+
if not new_token:
158+
raise ThreadsAPIError("API không trả về access_token mới khi refresh.")
159+
160+
self.access_token = new_token
161+
print_substep("✅ Đã refresh access token thành công!", style="bold green")
162+
return new_token
163+
except requests.RequestException as exc:
164+
raise ThreadsAPIError(
165+
"Không thể refresh token. Vui lòng lấy token mới từ "
166+
"Meta Developer Portal.\n"
167+
f"Chi tiết: {exc}"
168+
) from exc
44169

45170
def get_user_threads(self, user_id: Optional[str] = None, limit: int = 25) -> List[dict]:
46171
"""Lấy danh sách threads của user.
@@ -138,12 +263,51 @@ def get_threads_posts(POST_ID: str = None) -> dict:
138263
139264
Returns:
140265
Dict chứa thread content và replies.
266+
267+
Raises:
268+
ThreadsAPIError: Nếu token không hợp lệ hoặc API trả về lỗi.
269+
ValueError: Nếu không tìm thấy threads phù hợp.
141270
"""
142271
print_substep("Đang kết nối với Threads API...")
143272

144273
client = ThreadsClient()
145274
content = {}
146275

276+
# Bước 0: Validate token trước khi gọi API
277+
print_substep("Đang kiểm tra access token...")
278+
try:
279+
user_info = client.validate_token()
280+
print_substep(
281+
f"✅ Token hợp lệ - User: @{user_info.get('username', 'N/A')} "
282+
f"(ID: {user_info.get('id', 'N/A')})",
283+
style="bold green",
284+
)
285+
except ThreadsAPIError:
286+
# Token không hợp lệ → thử refresh
287+
print_substep(
288+
"⚠️ Token có thể đã hết hạn, đang thử refresh...",
289+
style="bold yellow",
290+
)
291+
try:
292+
client.refresh_token()
293+
user_info = client.validate_token()
294+
print_substep(
295+
f"✅ Token đã refresh - User: @{user_info.get('username', 'N/A')}",
296+
style="bold green",
297+
)
298+
except (ThreadsAPIError, requests.RequestException) as refresh_err:
299+
print_substep(
300+
"❌ Không thể xác thực hoặc refresh token.\n"
301+
" Vui lòng lấy token mới từ Meta Developer Portal:\n"
302+
" https://developers.facebook.com/docs/threads/get-started",
303+
style="bold red",
304+
)
305+
raise ThreadsAPIError(
306+
"Access token không hợp lệ hoặc đã hết hạn. "
307+
"Vui lòng cập nhật access_token trong config.toml. "
308+
f"Chi tiết: {refresh_err}"
309+
) from refresh_err
310+
147311
thread_config = settings.config["threads"]["thread"]
148312
max_comment_length = int(thread_config.get("max_comment_length", 500))
149313
min_comment_length = int(thread_config.get("min_comment_length", 1))
@@ -160,14 +324,36 @@ def get_threads_posts(POST_ID: str = None) -> dict:
160324
threads_list = client.get_user_threads(user_id=target_user, limit=25)
161325

162326
if not threads_list:
163-
print_substep("Không tìm thấy threads nào!", style="bold red")
164-
raise ValueError("No threads found")
327+
print_substep(
328+
"❌ Không tìm thấy threads nào!\n"
329+
" Kiểm tra các nguyên nhân sau:\n"
330+
f" - User ID đang dùng: {target_user}\n"
331+
" - User này có bài viết công khai không?\n"
332+
" - Token có quyền threads_basic_read?\n"
333+
" - Token có đúng cho user_id này không?",
334+
style="bold red",
335+
)
336+
raise ValueError(
337+
f"No threads found for user '{target_user}'. "
338+
"Verify the user has public posts and the access token has "
339+
"'threads_basic_read' permission."
340+
)
165341

166342
# Lọc theo từ khóa nếu có
167343
keywords = thread_config.get("keywords", "")
344+
unfiltered_count = len(threads_list)
168345
if keywords:
169346
keyword_list = [k.strip() for k in keywords.split(",") if k.strip()]
170-
threads_list = client.search_threads_by_keyword(threads_list, keyword_list)
347+
filtered = client.search_threads_by_keyword(threads_list, keyword_list)
348+
if filtered:
349+
threads_list = filtered
350+
else:
351+
# Nếu keyword filter loại hết → bỏ qua filter, dùng list gốc
352+
print_substep(
353+
f"⚠️ Keyword filter ({keywords}) loại hết {unfiltered_count} "
354+
"threads. Bỏ qua keyword filter, dùng tất cả threads.",
355+
style="bold yellow",
356+
)
171357

172358
# Chọn thread phù hợp (chưa tạo video, đủ replies, title chưa dùng)
173359
thread = None

0 commit comments

Comments
 (0)