| |
| import os |
| import time |
| import requests |
| from config import THREADS_ACCESS_TOKEN |
|
|
| THREADS_API_BASE = "https://graph.threads.net/v1.0" |
| LAST_ERROR = None |
|
|
| def upload_to_tmpfiles(file_path): |
| """ |
| Загружает локальный файл на tmpfiles.org и возвращает прямую ссылку на скачивание. |
| """ |
| url = "https://tmpfiles.org/api/v1/upload" |
| try: |
| print(f"[Threads API] Загрузка файла {os.path.basename(file_path)} на tmpfiles.org...") |
| with open(file_path, "rb") as f: |
| files = {"file": f} |
| r = requests.post(url, files=files, timeout=60) |
| if r.status_code == 200: |
| res_data = r.json() |
| if res_data.get("status") == "success": |
| view_url = res_data["data"]["url"] |
| |
| direct_url = view_url.replace("https://tmpfiles.org/", "https://tmpfiles.org/dl/") |
| print(f"[Threads API] Файл успешно загружен на tmpfiles.org. Ссылка: {direct_url}") |
| return direct_url |
| print(f"[Threads API Error] Ошибка загрузки на tmpfiles.org ({r.status_code}): {r.text}") |
| return None |
| except Exception as e: |
| print(f"[Threads API Exception] Исключение при загрузке на tmpfiles.org: {e}") |
| return None |
|
|
| def upload_to_catbox(file_path): |
| """ |
| Загружает локальный файл на бесплатный хостинг изображений Catbox.moe |
| и возвращает прямую ссылку. |
| """ |
| url = "https://catbox.moe/user/api.php" |
| payload = {"reqtype": "fileupload"} |
| try: |
| print(f"[Threads API] Загрузка файла {os.path.basename(file_path)} на Catbox...") |
| with open(file_path, "rb") as f: |
| files = {"fileToUpload": f} |
| r = requests.post(url, data=payload, files=files, timeout=60) |
| if r.status_code == 200: |
| public_url = r.text.strip() |
| |
| if public_url.startswith("http") and "catbox" in public_url: |
| print(f"[Threads API] Файл успешно загружен. Ссылка: {public_url}") |
| return public_url |
| print(f"[Threads API Error] Ошибка загрузки на Catbox ({r.status_code}): {r.text}") |
| return None |
| except Exception as e: |
| print(f"[Threads API Exception] Исключение при загрузке на Catbox: {e}") |
| return None |
|
|
| def upload_to_uguu(file_path): |
| """ |
| Загружает локальный файл на бесплатный хостинг uguu.se |
| """ |
| url = "https://uguu.se/upload" |
| try: |
| print(f"[Threads API] Загрузка файла {os.path.basename(file_path)} на Uguu.se...") |
| with open(file_path, "rb") as f: |
| files = {"files[]": f} |
| r = requests.post(url, files=files, timeout=60) |
| if r.status_code == 200: |
| res_data = r.json() |
| if res_data.get("success"): |
| public_url = res_data["files"][0]["url"] |
| print(f"[Threads API] Файл успешно загружен на Uguu.se. Ссылка: {public_url}") |
| return public_url |
| print(f"[Threads API Error] Ошибка загрузки на Uguu.se ({r.status_code}): {r.text}") |
| return None |
| except Exception as e: |
| print(f"[Threads API Exception] Исключение при загрузке на Uguu.se: {e}") |
| return None |
|
|
| def upload_image(file_path): |
| """ |
| Универсальная загрузка. Сначала tmpfiles.org, затем Catbox, затем Uguu.se. |
| Возвращает URL или None. |
| """ |
| url = upload_to_tmpfiles(file_path) |
| if url: |
| return url |
| |
| print("[Threads API Warning] tmpfiles.org сбоит. Переключение на резервный хостинг Catbox...") |
| url = upload_to_catbox(file_path) |
| if url: |
| return url |
| |
| print("[Threads API Warning] Catbox сбоит. Переключение на Uguu.se...") |
| return upload_to_uguu(file_path) |
|
|
| def create_media_container(payload): |
| """ |
| Создает медиа-контейнер в Threads API. |
| """ |
| global LAST_ERROR |
| url = f"{THREADS_API_BASE}/me/threads" |
| params = {"access_token": THREADS_ACCESS_TOKEN} |
| |
| try: |
| r = requests.post(url, params=params, json=payload, timeout=30) |
| if r.status_code == 200: |
| LAST_ERROR = None |
| container_id = r.json().get("id") |
| print(f"[Threads API] Создан контейнер: {container_id}") |
| return container_id |
| else: |
| LAST_ERROR = r.text |
| print(f"[Threads API Error] Ошибка создания контейнера ({r.status_code}): {r.text}") |
| return None |
| except Exception as e: |
| LAST_ERROR = str(e) |
| print(f"[Threads API Exception] Исключение при создании контейнера: {e}") |
| return None |
|
|
| def publish_media_container(container_id): |
| """ |
| Публикует ранее созданный медиа-контейнер. |
| """ |
| global LAST_ERROR |
| url = f"{THREADS_API_BASE}/me/threads_publish" |
| params = {"access_token": THREADS_ACCESS_TOKEN} |
| payload = {"creation_id": container_id} |
| |
| try: |
| r = requests.post(url, params=params, json=payload, timeout=30) |
| if r.status_code == 200: |
| LAST_ERROR = None |
| media_id = r.json().get("id") |
| print(f"[Threads API] Контейнер {container_id} успешно опубликован! Media ID: {media_id}") |
| return media_id |
| else: |
| LAST_ERROR = r.text |
| print(f"[Threads API Error] Ошибка публикации контейнера ({r.status_code}): {r.text}") |
| return None |
| except Exception as e: |
| LAST_ERROR = str(e) |
| print(f"[Threads API Exception] Исключение при публикации контейнера: {e}") |
| return None |
|
|
| def wait_for_container(container_id, max_retries=15, sleep_time=5): |
| """ |
| Периодически проверяет статус медиа-контейнера до получения FINISHED. |
| """ |
| url = f"{THREADS_API_BASE}/{container_id}" |
| params = { |
| "fields": "status,error_message", |
| "access_token": THREADS_ACCESS_TOKEN |
| } |
| |
| for i in range(max_retries): |
| try: |
| r = requests.get(url, params=params, timeout=10) |
| if r.status_code == 200: |
| data = r.json() |
| status = data.get("status") |
| if status == "FINISHED": |
| print(f"[Threads API] Контейнер {container_id} готов (FINISHED)!") |
| return True |
| elif status == "ERROR": |
| error_msg = data.get("error_message", "Unknown error") |
| print(f"[Threads API Error] Ошибка обработки контейнера {container_id}: {error_msg}") |
| return False |
| else: |
| print(f"[Threads API] Ожидание контейнера {container_id} (статус: {status})...") |
| else: |
| print(f"[Threads API] Ошибка проверки статуса: {r.text}") |
| except Exception as e: |
| print(f"[Threads API] Исключение при проверке статуса: {e}") |
| |
| time.sleep(sleep_time) |
| |
| print(f"[Threads API Error] Тайм-аут ожидания контейнера {container_id}") |
| return False |
|
|
| def publish_text_post(text, reply_to_id=None): |
| """ |
| Публикует текстовый пост (или комментарий/self-ответ, если передан reply_to_id). |
| """ |
| payload = { |
| "media_type": "TEXT", |
| "text": text |
| } |
| if reply_to_id: |
| payload["reply_to_id"] = reply_to_id |
| |
| container_id = create_media_container(payload) |
| if container_id: |
| |
| time.sleep(2) |
| return publish_media_container(container_id) |
| return None |
|
|
| def publish_image_post(image_path, text="", reply_to_id=None): |
| """ |
| Публикует одиночный пост с картинкой. |
| """ |
| image_url = upload_image(image_path) |
| if not image_url: |
| return None |
| |
| payload = { |
| "media_type": "IMAGE", |
| "image_url": image_url |
| } |
| if text: |
| payload["text"] = text |
| if reply_to_id: |
| payload["reply_to_id"] = reply_to_id |
| |
| container_id = create_media_container(payload) |
| if container_id: |
| print(f"[Threads API] Ожидание обработки изображения {container_id}...") |
| if wait_for_container(container_id, max_retries=10): |
| return publish_media_container(container_id) |
| else: |
| print("[Threads API Error] Отмена публикации из-за ошибки обработки.") |
| return None |
|
|
| def publish_carousel_post(image_paths, text="", reply_to_id=None): |
| """ |
| Публикует карусель изображений. |
| """ |
| child_ids = [] |
| |
| |
| for path in image_paths: |
| img_url = upload_image(path) |
| if not img_url: |
| print(f"[Threads API Error] Ошибка загрузки слайда {path}. Публикация карусели прервана.") |
| return None |
| |
| slide_payload = { |
| "media_type": "IMAGE", |
| "image_url": img_url, |
| "is_carousel_item": True |
| } |
| child_id = create_media_container(slide_payload) |
| if not child_id: |
| print(f"[Threads API Error] Ошибка создания контейнера для слайда {path}. Публикация карусели прервана.") |
| return None |
| child_ids.append(child_id) |
| |
| if not child_ids: |
| print("[Threads API Error] Не удалось создать слайды для карусели.") |
| return None |
| |
| |
| print(f"[Threads API] Ожидание обработки {len(child_ids)} слайдов-потомков...") |
| all_ready = True |
| for child_id in child_ids: |
| if not wait_for_container(child_id, max_retries=25): |
| print(f"[Threads API Error] Слайд {child_id} не был успешно обработан. Карусель не будет опубликована.") |
| all_ready = False |
| break |
| |
| if not all_ready: |
| return None |
| |
| |
| carousel_payload = { |
| "media_type": "CAROUSEL", |
| "children": child_ids |
| } |
| if text: |
| carousel_payload["text"] = text |
| if reply_to_id: |
| carousel_payload["reply_to_id"] = reply_to_id |
| |
| parent_container_id = create_media_container(carousel_payload) |
| if parent_container_id: |
| print(f"[Threads API] Ожидание обработки контейнера карусели {parent_container_id}...") |
| if wait_for_container(parent_container_id, max_retries=20): |
| return publish_media_container(parent_container_id) |
| else: |
| print("[Threads API Error] Карусель не опубликована из-за ошибки обработки.") |
| return None |
|
|
| def fetch_recent_posts(limit=10): |
| """ |
| Получает тексты последних постов пользователя из Threads API. |
| """ |
| url = f"{THREADS_API_BASE}/me/threads" |
| params = { |
| "fields": "id,text,timestamp,media_type", |
| "access_token": THREADS_ACCESS_TOKEN, |
| "limit": limit |
| } |
| try: |
| r = requests.get(url, params=params, timeout=20) |
| if r.status_code == 200: |
| data = r.json().get("data", []) |
| |
| texts = [item.get("text") for item in data if item.get("text")] |
| return texts |
| else: |
| print(f"[Threads API Error] Ошибка получения последних тредов ({r.status_code}): {r.text}") |
| return [] |
| except Exception as e: |
| print(f"[Threads API Exception] Исключение при получении последних тредов: {e}") |
| return [] |
|
|
|
|
|
|