feat(news): F1 — monitor RSS best-effort + comando /news manual
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s

src/news/monitor.py: 7 feeds UAP/OVNI, matching normalizado (NFKD sin tildes)
con word-boundary para tokens cortos (uap/ufo/nhi/aaro/ovni) y substring para
frases. poll_feeds aísla cada feed (uno caído nunca tumba el run) y parsea en
hilo aparte para no bloquear el event loop; cold-start por fuente (siembra sin
notificar). format_digest agrupa por fuente y trocea <4000 chars. /news refresca
y muestra novedades 24h. Capa desacoplada: src/news NO importa de src/bot.
No toca el scheduler (F2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-06-26 20:57:26 +00:00
co-authored by Claude Opus 4.8
parent fd814a3ccf
commit c0b0c1ba0a
4 changed files with 204 additions and 2 deletions
+29
View File
@@ -22,6 +22,7 @@ from src.db.database import get_db, close_db, ResearchDB, ResearchStatus, Output
from src.scraper.exhaustive import ExhaustiveScraper
from src.processor.processor import OllamaClient, ContentProcessor
from src.generator.generator import OutputGenerator
from src.news.monitor import poll_feeds, item_from_row, format_digest
logger = structlog.get_logger()
@@ -503,6 +504,33 @@ async def cmd_outputs(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await db_conn.close()
async def cmd_news(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Monitor RSS manual (sin scheduler — F2 lo automatiza). Refresca news_seen
(siembra en cold-start) y muestra las novedades matcheadas de las últimas 24h."""
if not is_authorized(update.effective_user.id):
return
db_conn = await get_db()
db = ResearchDB(db_conn)
try:
await update.message.reply_text("📡 Buscando novedades UAP/OVNI…")
try:
await poll_feeds(db, settings) # refresca tabla (siembra en cold-start)
except Exception:
logger.exception("news poll failed") # best-effort: nunca rompe el comando
recent = await db.get_recent_news(24)
if not recent:
await update.message.reply_text("Sin novedades UAP/OVNI en las últimas 24h.")
return
items = [item_from_row(r) for r in recent]
for chunk in format_digest(items):
await update.message.reply_text(chunk, disable_web_page_preview=False)
finally:
await db_conn.close()
async def cmd_costs(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not is_authorized(update.effective_user.id):
return
@@ -1292,6 +1320,7 @@ def create_bot() -> Application:
app.add_handler(CommandHandler("generate", cmd_generate))
app.add_handler(CommandHandler("sources", cmd_sources))
app.add_handler(CommandHandler("outputs", cmd_outputs))
app.add_handler(CommandHandler("news", cmd_news))
app.add_handler(CommandHandler("export", cmd_export))
app.add_handler(CommandHandler("costs", cmd_costs))
app.add_handler(CommandHandler("watch", cmd_watch))