Implementación inicial del sistema de automatización de redes sociales

- Estructura completa del proyecto con FastAPI
- Modelos de base de datos (productos, servicios, posts, calendario, interacciones)
- Publishers para X, Threads, Instagram, Facebook
- Generador de contenido con DeepSeek API
- Worker de Celery con tareas programadas
- Dashboard básico con templates HTML
- Docker Compose para despliegue
- Documentación completa

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 01:11:44 +00:00
commit 049d2133f9
53 changed files with 5876 additions and 0 deletions

View File

@@ -0,0 +1,227 @@
"""
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