diff --git a/app/publishers/base.py b/app/publishers/base.py index 78ae1c9..a306476 100644 --- a/app/publishers/base.py +++ b/app/publishers/base.py @@ -68,6 +68,17 @@ class BasePublisher(ABC): """Eliminar un post.""" 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: """Validar que el contenido cumple con los límites de la plataforma.""" # Implementar en subclases según límites específicos diff --git a/app/publishers/facebook_publisher.py b/app/publishers/facebook_publisher.py index ff4120f..7a78051 100644 --- a/app/publishers/facebook_publisher.py +++ b/app/publishers/facebook_publisher.py @@ -250,3 +250,46 @@ class FacebookPublisher(BasePublisher): 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 diff --git a/app/publishers/instagram_publisher.py b/app/publishers/instagram_publisher.py index d58bcd8..13f442e 100644 --- a/app/publishers/instagram_publisher.py +++ b/app/publishers/instagram_publisher.py @@ -238,3 +238,55 @@ class InstagramPublisher(BasePublisher): """Eliminar un post (no disponible vía API).""" # La API de Instagram no permite eliminar posts 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 diff --git a/app/publishers/threads_publisher.py b/app/publishers/threads_publisher.py index 2c7a0b1..d40a969 100644 --- a/app/publishers/threads_publisher.py +++ b/app/publishers/threads_publisher.py @@ -225,3 +225,31 @@ class ThreadsPublisher(BasePublisher): """Eliminar un post de Threads (no soportado actualmente).""" # La API de Threads no soporta eliminación actualmente 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 diff --git a/app/publishers/x_publisher.py b/app/publishers/x_publisher.py index 2172322..c8bad6b 100644 --- a/app/publishers/x_publisher.py +++ b/app/publishers/x_publisher.py @@ -278,6 +278,36 @@ class XPublisher(BasePublisher): except tweepy.TweepyException: 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]: """Obtener lista de followers recientes.""" if not self.client: