fix(tg): reviewer fixes — error handling, timeouts, db index

- commands.py: try/except TelegramBadRequest around all Bot API calls (#2);
  /new handles "topics limit" with user-friendly message (#4)
- start.py: isolate _check_and_prune_stale_topics with try/except Exception (#3)
- message.py: asyncio.timeout(30) around stream_message; handle TimeoutError (#6)
- db.py: add idx_chats_user_id index in init_db() (#7)
- settings.py: remove dead active_chat_id variable (#8)
- tests: add test_message.py (stream error/success); add 2 tests in test_commands.py
  (topics limit, /archive in General topic)
This commit is contained in:
Mikhail Putilovskij 2026-04-02 13:44:59 +03:00
parent c95360ce1f
commit 8901e60f6a
7 changed files with 161 additions and 25 deletions

View file

@ -29,6 +29,7 @@ def init_db() -> None:
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, thread_id)
);
CREATE INDEX IF NOT EXISTS idx_chats_user_id ON chats(user_id);
""")

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import structlog
from aiogram import Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import Command
from aiogram.types import Message
@ -20,7 +21,15 @@ async def cmd_new(message: Message) -> None:
chat_id = message.chat.id
n = db.count_active_chats(user_id) + 1
new_name = f"Чат #{n}"
topic = await message.bot.create_forum_topic(chat_id=chat_id, name=new_name)
try:
topic = await message.bot.create_forum_topic(chat_id=chat_id, name=new_name)
except TelegramBadRequest as e:
if "topics limit" in str(e).lower():
await message.answer("Достигнут лимит топиков (1000). Заархивируй неиспользуемые чаты.")
else:
logger.error("cmd_new_failed", error=str(e))
await message.answer("Не удалось создать чат, попробуй позже.")
return
thread_id = topic.message_thread_id
db.create_chat(user_id=user_id, thread_id=thread_id, chat_name=new_name)
await message.bot.send_message(
@ -40,7 +49,10 @@ async def cmd_archive(message: Message) -> None:
if chat is None or chat["archived_at"] is not None:
await message.answer("Этот чат не найден или уже архивирован.")
return
await message.bot.close_forum_topic(chat_id=message.chat.id, message_thread_id=thread_id)
try:
await message.bot.close_forum_topic(chat_id=message.chat.id, message_thread_id=thread_id)
except TelegramBadRequest as e:
logger.warning("cmd_archive_bot_error", error=str(e))
db.archive_chat(user_id=user_id, thread_id=thread_id)
logger.info("cmd_archive", user_id=user_id, thread_id=thread_id)
@ -59,11 +71,16 @@ async def cmd_rename(message: Message) -> None:
if chat is None:
await message.answer("Этот чат не найден.")
return
await message.bot.edit_forum_topic(
chat_id=message.chat.id,
message_thread_id=thread_id,
name=new_name[:128],
)
try:
await message.bot.edit_forum_topic(
chat_id=message.chat.id,
message_thread_id=thread_id,
name=new_name[:128],
)
except TelegramBadRequest as e:
logger.error("cmd_rename_failed", error=str(e))
await message.answer("Не удалось переименовать топик.")
return
db.rename_chat(user_id=user_id, thread_id=thread_id, new_name=new_name[:128])
logger.info("cmd_rename", user_id=user_id, thread_id=thread_id, new_name=new_name)

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import asyncio
import time
import structlog
@ -46,22 +47,26 @@ async def handle_topic_message(message: Message, dispatcher: EventDispatcher) ->
last_edit_len = 0
try:
async for chunk in dispatcher._platform.stream_message(
user_id=platform_user.user_id,
chat_id=str(thread_id),
text=incoming.text,
attachments=None,
):
accumulated += chunk.delta
now = time.monotonic()
delta = len(accumulated) - last_edit_len
if delta >= STREAM_MIN_DELTA and (now - last_edit_time) >= STREAM_EDIT_INTERVAL:
await _safe_edit(placeholder, accumulated)
last_edit_time = now
last_edit_len = len(accumulated)
async with asyncio.timeout(30):
async for chunk in dispatcher._platform.stream_message(
user_id=platform_user.user_id,
chat_id=str(thread_id),
text=incoming.text,
attachments=None,
):
accumulated += chunk.delta
now = time.monotonic()
delta = len(accumulated) - last_edit_len
if delta >= STREAM_MIN_DELTA and (now - last_edit_time) >= STREAM_EDIT_INTERVAL:
await _safe_edit(placeholder, accumulated)
last_edit_time = now
last_edit_len = len(accumulated)
await _safe_edit(placeholder, accumulated or "...")
await _safe_edit(placeholder, accumulated or "...")
except TimeoutError:
logger.warning("platform_timeout", user_id=user_id, thread_id=thread_id)
await _safe_edit(placeholder, "Сервис не отвечает, попробуй позже")
except TelegramBadRequest as e:
if "thread not found" in str(e).lower():
db.archive_chat(user_id=user_id, thread_id=thread_id)

View file

@ -34,9 +34,6 @@ async def cb_settings_back(callback: CallbackQuery, state: FSMContext) -> None:
@router.callback_query(F.data == "settings:skills")
async def cb_skills(callback: CallbackQuery, state: FSMContext, dispatcher: EventDispatcher) -> None:
data = await state.get_data()
active_chat_id = data.get("active_chat_id", "")
# Get platform user id
platform_user_id = str(callback.from_user.id)
settings = await dispatcher._platform.get_settings(platform_user_id)

View file

@ -24,7 +24,10 @@ async def cmd_start(message: Message) -> None:
user_id = message.from_user.id
chat_id = message.chat.id
await _check_and_prune_stale_topics(message, user_id, chat_id)
try:
await _check_and_prune_stale_topics(message, user_id, chat_id)
except Exception:
logger.exception("prune_stale_topics_error", user_id=user_id)
active = db.get_active_chats(user_id)