1 Commits
Author SHA1 Message Date
Renovate Bot 4dff236093 chore(deps): update dependency fastapi to v0.136.3 2026-05-27 12:00:45 +00:00
4 changed files with 25 additions and 118 deletions
+4 -4
View File
@@ -1,5 +1,5 @@
# Core
fastapi==0.137.0
fastapi==0.136.3
uvicorn==0.30.0
python-telegram-bot==21.5
httpx==0.27.0
@@ -8,14 +8,14 @@ aiohttp==3.10.0
# Scraping
beautifulsoup4==4.12.3
lxml==5.2.2
trafilatura==1.12.2
youtube-transcript-api==0.6.3
trafilatura==1.12.0
youtube-transcript-api==0.6.2
pdfplumber==0.11.9
feedparser==6.0.12
duckduckgo-search==6.2.6
# Storage & Embeddings
sqlite-vec==0.1.9
sqlite-vec==0.1.6
aiosqlite==0.20.0
# Processing
+18 -105
View File
@@ -5,9 +5,8 @@ Main user interface — all commands handled here
import asyncio
import os
import time
from datetime import datetime, timedelta
from datetime import datetime
from typing import Optional
from zoneinfo import ZoneInfo
import structlog
from telegram import Update, Message
@@ -537,62 +536,22 @@ async def cmd_costs(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await db_conn.close()
def _parse_at_time(time_str: str) -> tuple[float, int, int, bool]:
"""Parse 'HH:MM' in the configured timezone.
Returns (next_run_at_unix, hour, minute, is_today). If the time has
already passed today, schedules for tomorrow (is_today=False).
Raises ValueError if the string is not a valid HH:MM time.
"""
parts = time_str.split(":")
if len(parts) != 2 or not (parts[0].isdigit() and parts[1].isdigit()):
raise ValueError(f"Hora inválida: {time_str!r}")
hour, minute = int(parts[0]), int(parts[1])
if not (0 <= hour <= 23 and 0 <= minute <= 59):
raise ValueError(f"Hora fuera de rango: {time_str!r}")
tz = ZoneInfo(settings.timezone)
now_local = datetime.now(tz)
target = now_local.replace(hour=hour, minute=minute, second=0, microsecond=0)
is_today = True
if target <= now_local:
target += timedelta(days=1)
is_today = False
return target.timestamp(), hour, minute, is_today
async def cmd_watch(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not is_authorized(update.effective_user.id):
return
chat_id = update.effective_chat.id
args = list(ctx.args or [])
args = ctx.args or []
if not args:
await update.message.reply_text(
"❌ Uso: `/watch <tema> [horas]` o `/watch <tema> --at HH:MM [horas]`\n"
"Ejemplo: `/watch Incidente Roswell 24`\n"
"Ejemplo: `/watch Incidente Roswell --at 19:30 6`",
"❌ Uso: `/watch <tema> [horas]`\nEjemplo: `/watch Incidente Roswell 24`",
parse_mode=ParseMode.MARKDOWN
)
return
# Extract optional --at HH:MM from anywhere in the args
at_time_str: Optional[str] = None
if "--at" in args:
idx = args.index("--at")
if idx + 1 >= len(args):
await update.message.reply_text(
"❌ `--at` requiere una hora en formato HH:MM. Ejemplo: `--at 19:30`",
parse_mode=ParseMode.MARKDOWN
)
return
at_time_str = args[idx + 1]
# Remove '--at' and its value from args
del args[idx:idx + 2]
interval_hours = 24
if args and args[-1].isdigit():
if args[-1].isdigit():
interval_hours = int(args[-1])
topic = " ".join(args[:-1]).strip()
else:
@@ -608,31 +567,14 @@ async def cmd_watch(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
)
return
next_run_at: Optional[float] = None
when_msg = f"Primera ejecución en ~{interval_hours}h."
if at_time_str is not None:
try:
next_run_at, hour, minute, is_today = _parse_at_time(at_time_str)
except ValueError:
await update.message.reply_text(
"❌ Hora inválida. Usa el formato HH:MM (24h). Ejemplo: `--at 19:30`",
parse_mode=ParseMode.MARKDOWN
)
return
day_word = "hoy" if is_today else "mañana"
when_msg = (
f"Primera ejecución: {day_word} a las {hour:02d}:{minute:02d} "
f"({settings.timezone})"
)
db_conn = await get_db()
db = ResearchDB(db_conn)
try:
try:
await db.add_watch(topic, chat_id, interval_hours, next_run_at=next_run_at)
await db.add_watch(topic, chat_id, interval_hours)
await update.message.reply_text(
f"👁 Watching: `{topic}` — cada {interval_hours}h\n"
f"{when_msg}\n"
f"Primera ejecución en ~{interval_hours}h.\n"
f"Usa /watches para ver todos tus temas.",
parse_mode=ParseMode.MARKDOWN
)
@@ -691,27 +633,14 @@ async def cmd_watches(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
return
now = time.time()
tz = ZoneInfo(settings.timezone)
today_local = datetime.now(tz).date()
lines = ["👁 *Tus temas vigilados:*\n"]
for i, w in enumerate(watches, 1):
secs_remaining = max(0.0, w["next_run_at"] - now)
hours_remaining = secs_remaining / 3600
eta = f"{int(secs_remaining / 60)}min" if hours_remaining < 1 else f"{hours_remaining:.1f}h"
status = "" if w["enabled"] else ""
nxt = datetime.fromtimestamp(w["next_run_at"], tz)
if nxt.date() == today_local:
day_word = "hoy"
elif nxt.date() == today_local + timedelta(days=1):
day_word = "mañana"
else:
day_word = nxt.strftime("%d/%m")
local_time = nxt.strftime("%H:%M")
lines.append(
f"{i}. {status} `{w['topic']}` — cada {w['interval_hours']}h · "
f"próxima a las {local_time} ({day_word}) · en {eta}"
f"{i}. {status} `{w['topic']}` — cada {w['interval_hours']}h · próxima en {eta}"
)
await update.message.reply_text("\n".join(lines), parse_mode=ParseMode.MARKDOWN)
@@ -816,23 +745,6 @@ async def _purge_on_startup(app: Application) -> None:
await db_conn.close()
async def _safe_send(bot, chat_id, text: str):
"""Envía un mensaje, con fallback a texto plano si falla el parseo Markdown.
Los resúmenes generados por Claude suelen contener entidades Markdown
desbalanceadas (*, _, [, `) que hacen que Telegram rechace el mensaje con
BadRequest. Sin este fallback, el envío fallaba en silencio.
"""
try:
await bot.send_message(chat_id, text, parse_mode=ParseMode.MARKDOWN)
except Exception as e:
logger.warning("Envío Markdown falló, reintentando en texto plano", error=str(e))
try:
await bot.send_message(chat_id, text)
except Exception as e2:
logger.error("Envío en texto plano también falló", chat_id=chat_id, error=str(e2))
async def _scheduler_loop(app: Application):
while True:
db_conn = None
@@ -875,23 +787,24 @@ async def _scheduler_loop(app: Application):
)
if summary:
await _safe_send(app.bot, c, summary)
await app.bot.send_message(
c, summary, parse_mode=ParseMode.MARKDOWN
)
else:
await _safe_send(
app.bot, c,
f"🔄 *{t}* — sin novedades significativas esta vez."
await app.bot.send_message(
c,
f"🔄 *{t}* — sin novedades significativas esta vez.",
parse_mode=ParseMode.MARKDOWN
)
except Exception as e:
logger.error("Tarea programada falló",
topic=t, session_id=s, error=str(e))
finally:
await inner_db_conn.close()
task = asyncio.create_task(_task())
_active_tasks[chat_id] = task
await _safe_send(
app.bot, chat_id,
f"🔄 Investigación automática iniciada: `{topic}`"
await app.bot.send_message(
chat_id,
f"🔄 Investigación automática iniciada: `{topic}`",
parse_mode=ParseMode.MARKDOWN
)
except Exception as e:
logger.warning("Scheduler loop error", error=str(e))
-1
View File
@@ -44,7 +44,6 @@ class Settings(BaseSettings):
# App
log_level: str = Field("INFO", env="LOG_LEVEL")
timezone: str = Field("Europe/Madrid", env="TIMEZONE")
@property
def allowed_user_ids(self) -> list[int]:
+3 -8
View File
@@ -378,16 +378,13 @@ class ResearchDB:
# --- Watched Topics ---
async def add_watch(self, topic: str, chat_id: int, interval_hours: int,
next_run_at: Optional[float] = None) -> int:
async def add_watch(self, topic: str, chat_id: int, interval_hours: int) -> int:
now = time.time()
if next_run_at is None:
next_run_at = now + interval_hours * 3600
cursor = await self.db.execute(
"""INSERT OR REPLACE INTO watched_topics
(topic, chat_id, interval_hours, next_run_at, created_at)
VALUES (?, ?, ?, ?, ?)""",
(topic, chat_id, interval_hours, next_run_at, now)
(topic, chat_id, interval_hours, now + interval_hours * 3600, now)
)
await self.db.commit()
return cursor.lastrowid
@@ -442,7 +439,7 @@ class ResearchDB:
)
session_ids = [row[0] for row in await cursor.fetchall()]
counts = {"sessions": 0, "sources": 0, "chunks": 0, "outputs": 0, "api_usage": 0}
counts = {"sessions": 0, "sources": 0, "chunks": 0, "outputs": 0}
for sid in session_ids:
await self.db.execute(
@@ -453,8 +450,6 @@ class ResearchDB:
counts["chunks"] += cur.rowcount
cur = await self.db.execute("DELETE FROM outputs WHERE session_id = ?", (sid,))
counts["outputs"] += cur.rowcount
cur = await self.db.execute("DELETE FROM api_usage WHERE session_id = ?", (sid,))
counts["api_usage"] += cur.rowcount
cur = await self.db.execute("DELETE FROM sources WHERE session_id = ?", (sid,))
counts["sources"] += cur.rowcount
cur = await self.db.execute("DELETE FROM research_sessions WHERE id = ?", (sid,))