Files
social-media-automation/app/publishers/facebook_publisher.py
Consultoría AS 049d2133f9 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>
2026-01-28 01:11:44 +00:00

253 lines
8.1 KiB
Python

"""
Publisher para Facebook Pages (Meta Graph API).
"""
from typing import Optional, List, Dict
import httpx
from app.core.config import settings
from app.publishers.base import BasePublisher, PublishResult
class FacebookPublisher(BasePublisher):
"""Publisher para Facebook Pages usando Meta Graph API."""
platform = "facebook"
char_limit = 63206 # Límite real de Facebook
base_url = "https://graph.facebook.com/v18.0"
def __init__(self):
self.access_token = settings.META_ACCESS_TOKEN
self.page_id = settings.FACEBOOK_PAGE_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 Facebook Page."""
if not self.access_token or not self.page_id:
return PublishResult(
success=False,
error_message="Credenciales de Facebook no configuradas"
)
try:
async with httpx.AsyncClient() as client:
if image_path:
# Publicar con imagen
url = f"{self.base_url}/{self.page_id}/photos"
payload = {
"caption": content,
"url": image_path, # URL pública de la imagen
"access_token": self.access_token
}
else:
# Publicar solo texto
url = f"{self.base_url}/{self.page_id}/feed"
payload = {
"message": content,
"access_token": self.access_token
}
response = await client.post(url, data=payload)
response.raise_for_status()
post_id = response.json().get("id")
return PublishResult(
success=True,
post_id=post_id,
url=f"https://www.facebook.com/{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 como un solo post largo en Facebook."""
# Facebook no tiene threads, concatenamos el contenido
combined_content = "\n\n".join(posts)
# Usar la primera imagen si existe
image = images[0] if images else None
return await self.publish(combined_content, image)
async def reply(
self,
post_id: str,
content: str
) -> PublishResult:
"""Responder a un comentario en Facebook."""
if not self.access_token:
return PublishResult(
success=False,
error_message="Token de acceso no configurado"
)
try:
async with httpx.AsyncClient() as client:
url = f"{self.base_url}/{post_id}/comments"
payload = {
"message": content,
"access_token": self.access_token
}
response = await client.post(url, data=payload)
response.raise_for_status()
comment_id = response.json().get("id")
return PublishResult(
success=True,
post_id=comment_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/comentario."""
if not self.access_token:
return False
try:
async with httpx.AsyncClient() as client:
url = f"{self.base_url}/{post_id}/likes"
payload = {"access_token": self.access_token}
response = await client.post(url, data=payload)
response.raise_for_status()
return response.json().get("success", False)
except httpx.HTTPError:
return False
async def get_mentions(self, since_id: Optional[str] = None) -> List[Dict]:
"""Obtener menciones de la página."""
if not self.access_token or not self.page_id:
return []
try:
async with httpx.AsyncClient() as client:
url = f"{self.base_url}/{self.page_id}/tagged"
params = {
"fields": "id,message,from,created_time,permalink_url",
"access_token": self.access_token
}
response = await client.get(url, params=params)
response.raise_for_status()
data = response.json()
return data.get("data", [])
except httpx.HTTPError:
return []
async def get_comments(self, post_id: str) -> List[Dict]:
"""Obtener comentarios de un post."""
if not self.access_token:
return []
try:
async with httpx.AsyncClient() as client:
url = f"{self.base_url}/{post_id}/comments"
params = {
"fields": "id,message,from,created_time,like_count",
"access_token": self.access_token
}
response = await client.get(url, params=params)
response.raise_for_status()
data = response.json()
return data.get("data", [])
except httpx.HTTPError:
return []
async def get_page_messages(self) -> List[Dict]:
"""Obtener mensajes de la página (inbox)."""
if not self.access_token or not self.page_id:
return []
try:
async with httpx.AsyncClient() as client:
url = f"{self.base_url}/{self.page_id}/conversations"
params = {
"fields": "id,participants,messages{message,from,created_time}",
"access_token": self.access_token
}
response = await client.get(url, params=params)
response.raise_for_status()
data = response.json()
return data.get("data", [])
except httpx.HTTPError:
return []
async def send_message(self, recipient_id: str, message: str) -> PublishResult:
"""Enviar mensaje directo a un usuario."""
if not self.access_token or not self.page_id:
return PublishResult(
success=False,
error_message="Credenciales no configuradas"
)
try:
async with httpx.AsyncClient() as client:
url = f"{self.base_url}/{self.page_id}/messages"
payload = {
"recipient": {"id": recipient_id},
"message": {"text": message},
"access_token": self.access_token
}
response = await client.post(url, json=payload)
response.raise_for_status()
message_id = response.json().get("message_id")
return PublishResult(
success=True,
post_id=message_id
)
except httpx.HTTPError as e:
return PublishResult(
success=False,
error_message=str(e)
)
async def delete(self, post_id: str) -> bool:
"""Eliminar un post."""
if not self.access_token:
return False
try:
async with httpx.AsyncClient() as client:
url = f"{self.base_url}/{post_id}"
params = {"access_token": self.access_token}
response = await client.delete(url, params=params)
response.raise_for_status()
return response.json().get("success", False)
except httpx.HTTPError:
return False