# -*- coding: utf-8 -*- 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() # Проверим, что это валидный URL, а не HTML с ошибкой 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 = [] # 1. Создаем контейнеры для каждого слайда 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 # 1.5. Ждем окончания обработки всех слайдов-потомков 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 # 2. Создаем контейнер-родитель для карусели 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 []