Files
social-media-automation/app/publishers/facebook_publisher.py
Consultoría AS 67263e7ed9 feat: Add get_post_metrics to all publishers for analytics
- Add abstract get_post_metrics() method to BasePublisher
- Implement get_post_metrics() in XPublisher using Twitter API v2
  - Returns: likes, comments, shares, retweets, quotes, impressions
- Implement get_post_metrics() in ThreadsPublisher using Meta Graph API
- Implement get_post_metrics() in FacebookPublisher with insights
- Implement get_post_metrics() in InstagramPublisher with insights

This enables the fetch_post_metrics task to collect engagement data
from all platforms, populating the analytics dashboard.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 22:12:54 +00:00

296 lines
9.8 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
async def get_post_metrics(self, post_id: str) -> Optional[Dict]:
"""Obtener métricas de un post de Facebook."""
if not self.access_token:
return None
try:
async with httpx.AsyncClient() as client:
url = f"{self.base_url}/{post_id}"
params = {
"fields": "likes.summary(true),comments.summary(true),shares,insights.metric(post_impressions,post_engaged_users)",
"access_token": self.access_token
}
response = await client.get(url, params=params)
response.raise_for_status()
data = response.json()
# Extraer métricas
likes = data.get("likes", {}).get("summary", {}).get("total_count", 0)
comments = data.get("comments", {}).get("summary", {}).get("total_count", 0)
shares = data.get("shares", {}).get("count", 0)
# Insights (pueden no estar disponibles)
impressions = 0
reach = 0
insights = data.get("insights", {}).get("data", [])
for insight in insights:
if insight.get("name") == "post_impressions":
impressions = insight.get("values", [{}])[0].get("value", 0)
elif insight.get("name") == "post_engaged_users":
reach = insight.get("values", [{}])[0].get("value", 0)
return {
"likes": likes,
"comments": comments,
"shares": shares,
"impressions": impressions,
"reach": reach,
}
except httpx.HTTPError:
return None