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>
This commit is contained in:
@@ -68,6 +68,17 @@ class BasePublisher(ABC):
|
|||||||
"""Eliminar un post."""
|
"""Eliminar un post."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_post_metrics(self, post_id: str) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Obtener métricas de un post.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con métricas: likes, comments, shares, impressions, etc.
|
||||||
|
None si no se pueden obtener
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def validate_content(self, content: str) -> bool:
|
def validate_content(self, content: str) -> bool:
|
||||||
"""Validar que el contenido cumple con los límites de la plataforma."""
|
"""Validar que el contenido cumple con los límites de la plataforma."""
|
||||||
# Implementar en subclases según límites específicos
|
# Implementar en subclases según límites específicos
|
||||||
|
|||||||
@@ -250,3 +250,46 @@ class FacebookPublisher(BasePublisher):
|
|||||||
|
|
||||||
except httpx.HTTPError:
|
except httpx.HTTPError:
|
||||||
return False
|
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
|
||||||
|
|||||||
@@ -238,3 +238,55 @@ class InstagramPublisher(BasePublisher):
|
|||||||
"""Eliminar un post (no disponible vía API)."""
|
"""Eliminar un post (no disponible vía API)."""
|
||||||
# La API de Instagram no permite eliminar posts
|
# La API de Instagram no permite eliminar posts
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def get_post_metrics(self, post_id: str) -> Optional[Dict]:
|
||||||
|
"""Obtener métricas de un post de Instagram."""
|
||||||
|
if not self.access_token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
url = f"{self.base_url}/{post_id}"
|
||||||
|
params = {
|
||||||
|
"fields": "like_count,comments_count,insights.metric(impressions,reach,saved,shares)",
|
||||||
|
"access_token": self.access_token
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await client.get(url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Métricas básicas
|
||||||
|
likes = data.get("like_count", 0)
|
||||||
|
comments = data.get("comments_count", 0)
|
||||||
|
|
||||||
|
# Insights (pueden no estar disponibles para todos los posts)
|
||||||
|
impressions = 0
|
||||||
|
reach = 0
|
||||||
|
saved = 0
|
||||||
|
shares = 0
|
||||||
|
|
||||||
|
insights = data.get("insights", {}).get("data", [])
|
||||||
|
for insight in insights:
|
||||||
|
name = insight.get("name")
|
||||||
|
value = insight.get("values", [{}])[0].get("value", 0)
|
||||||
|
if name == "impressions":
|
||||||
|
impressions = value
|
||||||
|
elif name == "reach":
|
||||||
|
reach = value
|
||||||
|
elif name == "saved":
|
||||||
|
saved = value
|
||||||
|
elif name == "shares":
|
||||||
|
shares = value
|
||||||
|
|
||||||
|
return {
|
||||||
|
"likes": likes,
|
||||||
|
"comments": comments,
|
||||||
|
"shares": shares,
|
||||||
|
"impressions": impressions,
|
||||||
|
"reach": reach,
|
||||||
|
"saves": saved,
|
||||||
|
}
|
||||||
|
|
||||||
|
except httpx.HTTPError:
|
||||||
|
return None
|
||||||
|
|||||||
@@ -225,3 +225,31 @@ class ThreadsPublisher(BasePublisher):
|
|||||||
"""Eliminar un post de Threads (no soportado actualmente)."""
|
"""Eliminar un post de Threads (no soportado actualmente)."""
|
||||||
# La API de Threads no soporta eliminación actualmente
|
# La API de Threads no soporta eliminación actualmente
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def get_post_metrics(self, post_id: str) -> Optional[Dict]:
|
||||||
|
"""Obtener métricas de un post de Threads."""
|
||||||
|
if not self.access_token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
url = f"{self.base_url}/{post_id}"
|
||||||
|
params = {
|
||||||
|
"fields": "id,likes,replies,quotes,reposts",
|
||||||
|
"access_token": self.access_token
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await client.get(url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"likes": data.get("likes", 0),
|
||||||
|
"comments": data.get("replies", 0),
|
||||||
|
"shares": data.get("reposts", 0),
|
||||||
|
"quotes": data.get("quotes", 0),
|
||||||
|
"impressions": 0, # No disponible en API pública
|
||||||
|
}
|
||||||
|
|
||||||
|
except httpx.HTTPError:
|
||||||
|
return None
|
||||||
|
|||||||
@@ -278,6 +278,36 @@ class XPublisher(BasePublisher):
|
|||||||
except tweepy.TweepyException:
|
except tweepy.TweepyException:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def get_post_metrics(self, post_id: str) -> Optional[Dict]:
|
||||||
|
"""Obtener métricas de un tweet."""
|
||||||
|
if not self.client:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Obtener tweet con métricas públicas
|
||||||
|
tweet = self.client.get_tweet(
|
||||||
|
id=post_id,
|
||||||
|
tweet_fields=['public_metrics', 'created_at']
|
||||||
|
)
|
||||||
|
|
||||||
|
if not tweet.data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
metrics = tweet.data.public_metrics or {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"likes": metrics.get("like_count", 0),
|
||||||
|
"comments": metrics.get("reply_count", 0),
|
||||||
|
"shares": metrics.get("retweet_count", 0),
|
||||||
|
"retweets": metrics.get("retweet_count", 0),
|
||||||
|
"quotes": metrics.get("quote_count", 0),
|
||||||
|
"impressions": metrics.get("impression_count", 0),
|
||||||
|
"bookmarks": metrics.get("bookmark_count", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
except tweepy.TweepyException:
|
||||||
|
return None
|
||||||
|
|
||||||
async def get_followers(self, max_results: int = 100) -> List[Dict]:
|
async def get_followers(self, max_results: int = 100) -> List[Dict]:
|
||||||
"""Obtener lista de followers recientes."""
|
"""Obtener lista de followers recientes."""
|
||||||
if not self.client:
|
if not self.client:
|
||||||
|
|||||||
Reference in New Issue
Block a user