diff --git a/src/bot/bot.py b/src/bot/bot.py index 20de42d..696e210 100644 --- a/src/bot/bot.py +++ b/src/bot/bot.py @@ -5,7 +5,7 @@ Main user interface — all commands handled here import asyncio import os import time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Optional from zoneinfo import ZoneInfo @@ -879,6 +879,9 @@ async def _safe_send(bot, chat_id, text: str): async def _scheduler_loop(app: Application): + # Estado en memoria (no en DB): en un reinicio re-polla una vez, inofensivo + # porque solo se empujan items notified=0 y se marcan tras enviarlos. + _last_news_poll: Optional[datetime] = None while True: db_conn = None try: @@ -938,6 +941,40 @@ async def _scheduler_loop(app: Application): app.bot, chat_id, f"🔄 Investigación automática iniciada: `{topic}`" ) + + # --- Monitor de noticias (F2) — inerte si NEWS_ENABLED=False. + # Best-effort: un fallo del news-poll NUNCA tumba el scheduler de + # watched_topics (va en su propio try/except). + if settings.NEWS_ENABLED: + now = datetime.now(timezone.utc) + interval = timedelta(hours=settings.NEWS_POLL_INTERVAL_HOURS) + due_news = (_last_news_poll is None + or (now - _last_news_poll) >= interval) + if due_news: + _last_news_poll = now # marca antes de pollear: evita reintentos en bucle si falla + news_chat_id = settings.news_chat_id + if not news_chat_id: + logger.warning("NEWS_ENABLED pero sin chat destino; salto poll") + else: + try: + await poll_feeds(db, settings) # inserta novedades notified=0 + pending = await db.get_unnotified() # TODOS los pendientes + if pending: + shown = pending[:settings.NEWS_MAX_ITEMS] + extra = len(pending) - len(shown) + items = [item_from_row(r) for r in shown] + chunks = format_digest(items, header="🛸 Novedades UAP/OVNI") + if extra > 0 and chunks: + chunks[-1] += f"\n…y {extra} más." + for c in chunks: + await app.bot.send_message( + chat_id=news_chat_id, text=c, + disable_web_page_preview=False, + ) + # Marca todos los pendientes (incluidos los "…y N más") + await db.mark_news_notified([r["id"] for r in pending]) + except Exception as e: + logger.warning("News poll/notify falló", error=str(e)) except Exception as e: logger.warning("Scheduler loop error", error=str(e)) finally: diff --git a/src/db/database.py b/src/db/database.py index 2f92595..09d4102 100644 --- a/src/db/database.py +++ b/src/db/database.py @@ -562,6 +562,23 @@ class ResearchDB: rows = await cursor.fetchall() return [dict(r) for r in rows] + async def get_unnotified(self, limit: Optional[int] = None) -> list[dict]: + """Items aún sin notificar (notified=0), más recientes primero. El + scheduler (F2) los empuja a Telegram y luego los marca con + mark_news_notified. Aplica LIMIT solo si `limit` no es None.""" + sql = ( + "SELECT id, source, title, link, published_at, matched_keywords " + "FROM news_seen WHERE notified = 0 " + "ORDER BY published_at DESC NULLS LAST, seen_at DESC" + ) + params: tuple = () + if limit is not None: + sql += " LIMIT ?" + params = (int(limit),) + cursor = await self.db.execute(sql, params) + rows = await cursor.fetchall() + return [dict(r) for r in rows] + # --- Maintenance --- async def purge_old_sessions(self, max_age_days: int = 30) -> dict: