""" Publisher para Threads (Meta). """ from typing import Optional, List, Dict import httpx from app.core.config import settings from app.publishers.base import BasePublisher, PublishResult class ThreadsPublisher(BasePublisher): """Publisher para Threads usando Meta Graph API.""" platform = "threads" char_limit = 500 base_url = "https://graph.threads.net/v1.0" def __init__(self): self.access_token = settings.META_ACCESS_TOKEN self.user_id = settings.THREADS_USER_ID def validate_content(self, content: str) -> bool: """Validar longitud del post.""" return len(content) <= self.char_limit async def publish( self, content: str, image_path: Optional[str] = None ) -> PublishResult: """Publicar en Threads.""" if not self.access_token or not self.user_id: return PublishResult( success=False, error_message="Credenciales de Threads no configuradas" ) try: async with httpx.AsyncClient() as client: # Paso 1: Crear el contenedor del post create_url = f"{self.base_url}/{self.user_id}/threads" payload = { "text": content, "media_type": "TEXT", "access_token": self.access_token } # Si hay imagen, subirla primero if image_path: # TODO: Implementar subida de imagen a Threads payload["media_type"] = "IMAGE" # payload["image_url"] = uploaded_image_url response = await client.post(create_url, data=payload) response.raise_for_status() container_id = response.json().get("id") # Paso 2: Publicar el contenedor publish_url = f"{self.base_url}/{self.user_id}/threads_publish" publish_payload = { "creation_id": container_id, "access_token": self.access_token } response = await client.post(publish_url, data=publish_payload) response.raise_for_status() post_id = response.json().get("id") return PublishResult( success=True, post_id=post_id, url=f"https://www.threads.net/post/{post_id}" ) except httpx.HTTPError as e: return PublishResult( success=False, error_message=str(e) ) async def publish_thread( self, posts: List[str], images: Optional[List[str]] = None ) -> PublishResult: """Publicar un hilo en Threads.""" if not self.access_token or not self.user_id: return PublishResult( success=False, error_message="Credenciales de Threads no configuradas" ) try: first_post_id = None reply_to_id = None for i, post in enumerate(posts): async with httpx.AsyncClient() as client: create_url = f"{self.base_url}/{self.user_id}/threads" payload = { "text": post, "media_type": "TEXT", "access_token": self.access_token } if reply_to_id: payload["reply_to_id"] = reply_to_id response = await client.post(create_url, data=payload) response.raise_for_status() container_id = response.json().get("id") # Publicar publish_url = f"{self.base_url}/{self.user_id}/threads_publish" publish_payload = { "creation_id": container_id, "access_token": self.access_token } response = await client.post(publish_url, data=publish_payload) response.raise_for_status() post_id = response.json().get("id") if i == 0: first_post_id = post_id reply_to_id = post_id return PublishResult( success=True, post_id=first_post_id, url=f"https://www.threads.net/post/{first_post_id}" ) except httpx.HTTPError as e: return PublishResult( success=False, error_message=str(e) ) async def reply( self, post_id: str, content: str ) -> PublishResult: """Responder a un post en Threads.""" if not self.access_token or not self.user_id: return PublishResult( success=False, error_message="Credenciales de Threads no configuradas" ) try: async with httpx.AsyncClient() as client: create_url = f"{self.base_url}/{self.user_id}/threads" payload = { "text": content, "media_type": "TEXT", "reply_to_id": post_id, "access_token": self.access_token } response = await client.post(create_url, data=payload) response.raise_for_status() container_id = response.json().get("id") # Publicar publish_url = f"{self.base_url}/{self.user_id}/threads_publish" publish_payload = { "creation_id": container_id, "access_token": self.access_token } response = await client.post(publish_url, data=publish_payload) response.raise_for_status() reply_id = response.json().get("id") return PublishResult( success=True, post_id=reply_id ) except httpx.HTTPError as e: return PublishResult( success=False, error_message=str(e) ) async def like(self, post_id: str) -> bool: """Dar like a un post (no soportado actualmente por la API).""" # La API de Threads no soporta likes programáticos actualmente return False async def get_mentions(self, since_id: Optional[str] = None) -> List[Dict]: """Obtener menciones (limitado en la API de Threads).""" # TODO: Implementar cuando la API lo soporte return [] async def get_comments(self, post_id: str) -> List[Dict]: """Obtener respuestas a un post.""" if not self.access_token: return [] try: async with httpx.AsyncClient() as client: url = f"{self.base_url}/{post_id}/replies" params = { "access_token": self.access_token, "fields": "id,text,username,timestamp" } response = await client.get(url, params=params) response.raise_for_status() data = response.json() return data.get("data", []) except httpx.HTTPError: return [] async def delete(self, post_id: str) -> bool: """Eliminar un post de Threads (no soportado actualmente).""" # La API de Threads no soporta eliminación actualmente return False