Download threads_api.py from FincH12/Automation_Threads_EU: direct link, hf CLI and curl.
- Browser
- Download file 13.2 kB
-
https://huggingface.co/spaces/FincH12/Automation_Threads_EU/resolve/main/threads_api.py
- Command line
-
hf download hf://spaces/FincH12/Automation_Threads_EU/threads_api.py
-
curl -L -o threads_api.py https://huggingface.co/spaces/FincH12/Automation_Threads_EU/resolve/main/threads_api.py
13.2 kB
| # -*- 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 [] | |