File size: 13,233 Bytes
9963647
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# -*- 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 []