feat: Add daily Telegram reports (morning and afternoon)

- Morning report (8:00 AM): scheduled posts for today + new interactions
- Afternoon report (6:00 PM): published count + pending interactions
- Reports are sent via Telegram with formatted summaries

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 10:10:31 +00:00
parent 5f04aa0cce
commit f5c6554fc3
2 changed files with 209 additions and 1 deletions

View File

@@ -17,7 +17,8 @@ celery_app = Celery(
"worker.tasks.publish_post", "worker.tasks.publish_post",
"worker.tasks.fetch_interactions", "worker.tasks.fetch_interactions",
"worker.tasks.cleanup", "worker.tasks.cleanup",
"worker.tasks.content_memory" "worker.tasks.content_memory",
"worker.tasks.daily_reports"
] ]
) )
@@ -77,4 +78,16 @@ celery_app.conf.beat_schedule = {
"task": "worker.tasks.content_memory.refresh_best_posts_yaml", "task": "worker.tasks.content_memory.refresh_best_posts_yaml",
"schedule": crontab(day_of_week=0, hour=5, minute=0), # Domingos 5 AM "schedule": crontab(day_of_week=0, hour=5, minute=0), # Domingos 5 AM
}, },
# Reporte matutino por Telegram (8:00 AM Tijuana)
"telegram-morning-report": {
"task": "worker.tasks.daily_reports.morning_report",
"schedule": crontab(hour=8, minute=0),
},
# Reporte vespertino por Telegram (6:00 PM Tijuana)
"telegram-afternoon-report": {
"task": "worker.tasks.daily_reports.afternoon_report",
"schedule": crontab(hour=18, minute=0),
},
} }

View File

@@ -0,0 +1,195 @@
"""
Tareas de reportes diarios por Telegram.
"""
import asyncio
from datetime import datetime, timedelta
from worker.celery_app import celery_app
from app.core.database import SessionLocal
from app.models.post import Post
from app.models.interaction import Interaction
from app.services.notifications import telegram_notify
def run_async(coro):
"""Helper para ejecutar coroutines en Celery."""
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro)
@celery_app.task(name="worker.tasks.daily_reports.morning_report")
def morning_report():
"""
Reporte matutino: posts programados para hoy + nuevas interacciones.
Se ejecuta a las 8:00 AM.
"""
db = SessionLocal()
try:
now = datetime.utcnow()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
today_end = today_start + timedelta(days=1)
# Posts programados para hoy
scheduled_posts = db.query(Post).filter(
Post.status == "scheduled",
Post.scheduled_at >= today_start,
Post.scheduled_at < today_end
).order_by(Post.scheduled_at).all()
# Interacciones de las últimas 24 horas sin responder
yesterday = now - timedelta(hours=24)
new_interactions = db.query(Interaction).filter(
Interaction.interaction_at >= yesterday,
Interaction.responded == False,
Interaction.is_archived == False
).order_by(Interaction.interaction_at.desc()).all()
# Construir mensaje
message = "☀️ *Buenos días! Reporte matutino*\n"
message += f"📅 {now.strftime('%d/%m/%Y')}\n\n"
# Sección de posts programados
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += "📝 *Posts programados para hoy:*\n\n"
if scheduled_posts:
for post in scheduled_posts:
time_str = post.scheduled_at.strftime("%H:%M")
platforms = ", ".join(post.platforms) if post.platforms else "N/A"
content_preview = post.content[:60] + "..." if len(post.content) > 60 else post.content
# Escapar caracteres especiales de Markdown
content_preview = content_preview.replace("_", "\\_").replace("*", "\\*")
message += f"⏰ *{time_str}* ({platforms})\n"
message += f" {content_preview}\n\n"
else:
message += " _No hay posts programados para hoy_\n\n"
# Sección de interacciones
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += "💬 *Nuevas interacciones (24h):*\n\n"
if new_interactions:
# Agrupar por plataforma
by_platform = {}
for interaction in new_interactions:
platform = interaction.platform
if platform not in by_platform:
by_platform[platform] = []
by_platform[platform].append(interaction)
for platform, interactions in by_platform.items():
message += f"📱 *{platform.upper()}* ({len(interactions)})\n"
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"
if len(interactions) > 3:
message += f" _...y {len(interactions) - 3} más_\n"
message += "\n"
else:
message += " _No hay nuevas interacciones_\n\n"
# Resumen
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += f"📊 *Resumen:* {len(scheduled_posts)} posts | {len(new_interactions)} interacciones"
# Enviar mensaje
success = run_async(telegram_notify(message))
return f"Reporte matutino enviado: {len(scheduled_posts)} posts, {len(new_interactions)} interacciones"
except Exception as e:
return f"Error en reporte matutino: {e}"
finally:
db.close()
@celery_app.task(name="worker.tasks.daily_reports.afternoon_report")
def afternoon_report():
"""
Reporte vespertino: solo actualización de interacciones.
Se ejecuta a las 6:00 PM.
"""
db = SessionLocal()
try:
now = datetime.utcnow()
# Interacciones desde la mañana (últimas 10 horas aprox)
since_morning = now - timedelta(hours=10)
new_interactions = db.query(Interaction).filter(
Interaction.interaction_at >= since_morning,
Interaction.responded == False,
Interaction.is_archived == False
).order_by(Interaction.interaction_at.desc()).all()
# Posts publicados hoy
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
published_today = db.query(Post).filter(
Post.status == "published",
Post.published_at >= today_start
).count()
# Posts pendientes para mañana
tomorrow_start = today_start + timedelta(days=1)
tomorrow_end = tomorrow_start + timedelta(days=1)
scheduled_tomorrow = db.query(Post).filter(
Post.status == "scheduled",
Post.scheduled_at >= tomorrow_start,
Post.scheduled_at < tomorrow_end
).count()
# Construir mensaje
message = "🌆 *Reporte vespertino*\n"
message += f"📅 {now.strftime('%d/%m/%Y %H:%M')}\n\n"
# Resumen del día
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += "📊 *Resumen del día:*\n\n"
message += f" ✅ Publicados hoy: {published_today}\n"
message += f" 📅 Programados mañana: {scheduled_tomorrow}\n\n"
# Interacciones pendientes
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += "💬 *Interacciones pendientes:*\n\n"
if new_interactions:
by_platform = {}
for interaction in new_interactions:
platform = interaction.platform
if platform not in by_platform:
by_platform[platform] = []
by_platform[platform].append(interaction)
for platform, interactions in by_platform.items():
message += f"📱 *{platform.upper()}* ({len(interactions)})\n"
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"
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*"
else:
message += " ✅ _Todas las interacciones han sido atendidas_\n\n"
message += "━━━━━━━━━━━━━━━━━━━━━\n"
message += "🎉 *Excelente trabajo hoy!*"
# Enviar mensaje
success = run_async(telegram_notify(message))
return f"Reporte vespertino enviado: {published_today} publicados, {len(new_interactions)} interacciones"
except Exception as e:
return f"Error en reporte vespertino: {e}"
finally:
db.close()