- 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)
78 lines
2.8 KiB
Python
78 lines
2.8 KiB
Python
from __future__ import annotations
|
||
|
||
import structlog
|
||
from aiogram import Router
|
||
from aiogram.exceptions import TelegramBadRequest
|
||
from aiogram.filters import CommandStart
|
||
from aiogram.types import Message
|
||
|
||
from adapter.telegram import db
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
router = Router(name="start")
|
||
|
||
|
||
@router.message(CommandStart())
|
||
async def cmd_start(message: Message) -> None:
|
||
"""
|
||
Bootstrap the user's forum.
|
||
|
||
First visit: create Чат #1, hide General topic.
|
||
Returning visit: health-check all active topics, archive stale ones.
|
||
"""
|
||
user_id = message.from_user.id
|
||
chat_id = message.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)
|
||
|
||
if not active:
|
||
try:
|
||
topic = await message.bot.create_forum_topic(chat_id=chat_id, name="Чат #1")
|
||
thread_id = topic.message_thread_id
|
||
db.create_chat(user_id=user_id, thread_id=thread_id, chat_name="Чат #1")
|
||
logger.info("start_created_first_topic", user_id=user_id, thread_id=thread_id)
|
||
except TelegramBadRequest as e:
|
||
logger.warning("start_create_topic_failed", error=str(e))
|
||
await message.answer(
|
||
"Не удалось создать топик. Убедись, что в @BotFather включён "
|
||
"Threaded Mode для этого бота."
|
||
)
|
||
return
|
||
|
||
try:
|
||
await message.bot.hide_general_forum_topic(chat_id=chat_id)
|
||
except TelegramBadRequest:
|
||
pass # Not critical
|
||
|
||
await message.answer(
|
||
"Привет! Это твоё личное пространство с AI-агентом Lambda. "
|
||
"Каждый топик — отдельный контекст. Напиши что-нибудь."
|
||
)
|
||
else:
|
||
await message.answer(
|
||
f"Снова привет! У тебя {len(active)} активных чатов. "
|
||
"Напиши /new чтобы создать новый."
|
||
)
|
||
|
||
|
||
async def _check_and_prune_stale_topics(
|
||
message: Message, user_id: int, chat_id: int
|
||
) -> None:
|
||
"""Send typing action to each active topic; archive any that no longer exist."""
|
||
for chat in db.get_active_chats(user_id):
|
||
thread_id = chat["thread_id"]
|
||
try:
|
||
await message.bot.send_chat_action(
|
||
chat_id=chat_id,
|
||
action="typing",
|
||
message_thread_id=thread_id,
|
||
)
|
||
except TelegramBadRequest:
|
||
db.archive_chat(user_id=user_id, thread_id=thread_id)
|
||
logger.info("pruned_stale_topic", user_id=user_id, thread_id=thread_id)
|