feat: Add follower tracking to interactions

- Add get_followers() method to X publisher
- Track new followers as "follow" interaction type
- Update daily reports to show followers separately
- Store follower name, username, bio, and profile image

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 23:07:28 +00:00
parent f5c6554fc3
commit b6823651e0
3 changed files with 113 additions and 9 deletions

View File

@@ -277,3 +277,38 @@ class XPublisher(BasePublisher):
return True
except tweepy.TweepyException:
return False
async def get_followers(self, max_results: int = 100) -> List[Dict]:
"""Obtener lista de followers recientes."""
if not self.client:
return []
try:
# Obtener ID del usuario autenticado
me = self.client.get_me()
user_id = me.data.id
# Obtener followers
followers = self.client.get_users_followers(
id=user_id,
max_results=max_results,
user_fields=['username', 'name', 'created_at', 'profile_image_url', 'description']
)
if not followers.data:
return []
return [
{
"id": str(user.id),
"username": user.username,
"name": user.name,
"description": user.description,
"profile_image_url": user.profile_image_url,
"created_at": user.created_at.isoformat() if user.created_at else None
}
for user in followers.data
]
except tweepy.TweepyException:
return []

View File

@@ -67,14 +67,30 @@ def morning_report():
else:
message += " _No hay posts programados para hoy_\n\n"
# Sección de interacciones
# Separar follows de otras interacciones
follows = [i for i in new_interactions if i.interaction_type == "follow"]
other_interactions = [i for i in new_interactions if i.interaction_type != "follow"]
# Sección de nuevos followers
if follows:
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += f"👥 *Nuevos followers ({len(follows)}):*\n\n"
for f in follows[:5]:
name = f.author_name or f.author_username
name = name.replace("_", "\\_").replace("*", "\\*") if name else "Usuario"
message += f" • @{f.author_username} ({name})\n"
if len(follows) > 5:
message += f" _...y {len(follows) - 5} más_\n"
message += "\n"
# Sección de otras interacciones
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += "💬 *Nuevas interacciones (24h):*\n\n"
if new_interactions:
if other_interactions:
# Agrupar por plataforma
by_platform = {}
for interaction in new_interactions:
for interaction in other_interactions:
platform = interaction.platform
if platform not in by_platform:
by_platform[platform] = []
@@ -85,7 +101,8 @@ def morning_report():
for i in interactions[:3]: # Mostrar máximo 3 por plataforma
content_preview = i.content[:50] + "..." if i.content and len(i.content) > 50 else (i.content or "N/A")
content_preview = content_preview.replace("_", "\\_").replace("*", "\\*")
message += f" • @{i.author_username}: {content_preview}\n"
type_emoji = "💬" if i.interaction_type == "comment" else "📢"
message += f" {type_emoji} @{i.author_username}: {content_preview}\n"
if len(interactions) > 3:
message += f" _...y {len(interactions) - 3} más_\n"
message += "\n"
@@ -154,13 +171,29 @@ def afternoon_report():
message += f" ✅ Publicados hoy: {published_today}\n"
message += f" 📅 Programados mañana: {scheduled_tomorrow}\n\n"
# Separar follows de otras interacciones
follows = [i for i in new_interactions if i.interaction_type == "follow"]
other_interactions = [i for i in new_interactions if i.interaction_type != "follow"]
# Nuevos followers del día
if follows:
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += f"👥 *Nuevos followers hoy ({len(follows)}):*\n\n"
for f in follows[:5]:
name = f.author_name or f.author_username
name = name.replace("_", "\\_").replace("*", "\\*") if name else "Usuario"
message += f" • @{f.author_username} ({name})\n"
if len(follows) > 5:
message += f" _...y {len(follows) - 5} más_\n"
message += "\n"
# Interacciones pendientes
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += "💬 *Interacciones pendientes:*\n\n"
if new_interactions:
if other_interactions:
by_platform = {}
for interaction in new_interactions:
for interaction in other_interactions:
platform = interaction.platform
if platform not in by_platform:
by_platform[platform] = []
@@ -171,13 +204,14 @@ def afternoon_report():
for i in interactions[:5]: # Mostrar más en el reporte de la tarde
content_preview = i.content[:50] + "..." if i.content and len(i.content) > 50 else (i.content or "N/A")
content_preview = content_preview.replace("_", "\\_").replace("*", "\\*")
message += f" • @{i.author_username}: {content_preview}\n"
type_emoji = "💬" if i.interaction_type == "comment" else "📢"
message += f" {type_emoji} @{i.author_username}: {content_preview}\n"
if len(interactions) > 5:
message += f" _...y {len(interactions) - 5} más_\n"
message += "\n"
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += f"⚠️ *{len(new_interactions)} interacciones sin responder*"
message += f"⚠️ *{len(other_interactions)} interacciones sin responder*"
else:
message += " ✅ _Todas las interacciones han sido atendidas_\n\n"
message += "━━━━━━━━━━━━━━━━━━━━━\n"

View File

@@ -138,9 +138,44 @@ def fetch_platform_interactions(platform: str):
processed_ids.add(external_id)
new_comments += 1
# Obtener nuevos followers (solo para X por ahora)
new_follows = 0
if platform == "x" and hasattr(publisher, 'get_followers'):
followers = run_async(publisher.get_followers(max_results=100))
for follower in followers:
# Usar "follow_" prefix para distinguir de tweet IDs
follower_id = follower.get("id")
external_id = f"follow_{follower_id}"
if external_id in processed_ids:
continue
existing = db.query(Interaction).filter(
Interaction.external_id == external_id
).first()
if not existing:
interaction = Interaction(
platform=platform,
interaction_type="follow",
external_id=external_id,
author_username=follower.get("username", "unknown"),
author_name=follower.get("name"),
author_avatar_url=follower.get("profile_image_url"),
content=follower.get("description"), # Bio del usuario
interaction_at=datetime.utcnow() # No sabemos cuándo siguió exactamente
)
db.add(interaction)
processed_ids.add(external_id)
new_follows += 1
db.commit()
return f"{platform}: {new_mentions} menciones, {new_comments} comentarios nuevos"
result = f"{platform}: {new_mentions} menciones, {new_comments} comentarios"
if new_follows > 0:
result += f", {new_follows} follows nuevos"
return result
except Exception as e:
db.rollback()