feat: /help разбита на разделы с навигацией инлайн-кнопками (aiogram 3.31.0)

- requirements.txt: aiogram 3.26.0 -> 3.31.0 (нативный send_rich_message + edit_message_text(rich_message=...))
- help_message.py: HELP_RICH_HTML -> HELP_SECTIONS (6 разделов) + help_menu_html/help_section_html/help_view/help_plain_view
- навигация на HTML-тегах <tg-button-row>/<tg-button type="callback_data"> (в меню 2 кнопки в ряд, в разделе «В меню» + «Предыдущий»/«Следующий»)
- bot.py: самописный raw-запрос sendRichMessage заменён на bot.send_rich_message; edit_rich_message + callback help_navigation
- онбординг по-прежнему вызывает help_command — кнопки появляются и там
- тест поймал реальный баг: в help_navigation пропущен await у edit_rich_message
- 58 тестов, VERSION 0.1.0+23
This commit is contained in:
admin committed 2026-10-06 23:15:47 +02:00
1 parent 0ff0e1abe7
commit c85f0a34fe
7 files changed
+236 -69

No files matched your search

+40 -33
View File
@@ -8,17 +8,16 @@ import sqlite3
from contextlib import asynccontextmanager, nullcontext
from pathlib import Path
import aiohttp
from aiogram import Bot, Dispatcher, F, Router
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TEST
from aiogram.filters import CommandStart
from aiogram.types import Message
from aiogram.types import CallbackQuery, InputRichMessage, Message
import user_store
from apinex_client import APINEX_STT_URL, POST_HISTORY_CHAR_LIMIT, POST_HISTORY_LIMIT, classify_intent, create_tone_profile, extract_posts_from_images, extract_transcript, generate_ideas, generate_posts, transcribe_audio
from help_message import HELP_RICH_HTML, WELCOME_RICH_HTML, rich_to_plain
from help_message import WELCOME_RICH_HTML, help_plain_view, help_view, rich_to_plain
from user_store import (
add_post_samples,
clear_user_data,
@@ -114,41 +113,49 @@ async def notify_development_deploy(bot: Bot) -> None:
logger.info("Development deploy notice sent: version=%s", VERSION)
async def send_rich_html(message: Message, rich_html: str, stage: str = "помощи") -> bool:
async def send_rich_message(message: Message, rich_html: str, stage: str) -> Message | None:
bot = message.bot
payload = {"chat_id": message.chat.id, "rich_message": {"html": rich_html}}
if message.message_thread_id is not None:
payload["message_thread_id"] = message.message_thread_id
url = bot.session.api.api_url(token=bot.token, method="sendRichMessage")
timeout = aiohttp.ClientTimeout(total=15)
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(
url, json=payload,
headers={"User-Agent": "Mozilla/5.0 (compatible; SocialPostWriterBot/1.0)"},
) as response:
data = await response.json(content_type=None)
if response.status == 200 and data.get("ok"):
return True
logger.warning("Rich %s rejected with HTTP %s", stage, response.status)
return False
sent = await bot.send_rich_message(
chat_id=message.chat.id,
rich_message=InputRichMessage(html=rich_html),
message_thread_id=message.message_thread_id,
)
return sent
except Exception as exc:
logger.warning("Rich %s delivery error (%s): %s", stage, type(exc).__name__, str(exc)[:160])
return False
async def send_rich_message(message: Message, rich_html: str, stage: str) -> None:
try:
sent = await send_rich_html(message, rich_html, stage)
except Exception as exc:
sent = False
logger.warning("Rich %s send failed (%s): %s", stage, type(exc).__name__, str(exc)[:180])
if DEPLOY_ENV == "development":
await report_error(message, f"отправка Rich Message: {stage}", exc)
if not sent:
if DEPLOY_ENV == "development":
await report_error(message, f"отправка Rich Message: {stage}", RuntimeError("sendRichMessage returned false"))
await message.answer(rich_to_plain(rich_html))
await message.answer(help_plain_view(None) if stage == "помощи" else rich_to_plain(rich_html))
return None
async def edit_rich_message(message: Message, rich_html: str) -> bool:
bot = message.bot
if message.message_id is None:
return False
try:
await bot.edit_message_text(
chat_id=message.chat.id,
message_id=message.message_id,
rich_message=InputRichMessage(html=rich_html),
)
return True
except Exception as exc:
logger.warning("Rich help edit failed (%s): %s", type(exc).__name__, str(exc)[:180])
return False
@router.callback_query(F.data.startswith("help:"))
async def help_navigation(callback: CallbackQuery) -> None:
message = callback.message
if not isinstance(message, Message):
return
section = (callback.data or "help:menu").split(":", 1)[1]
if not await edit_rich_message(message, help_view(section)):
await callback.answer("Не удалось обновить справку, открой /help заново", show_alert=True)
return
await callback.answer()
@router.message(CommandStart())
@@ -234,7 +241,7 @@ async def clear_command(message: Message) -> None:
async def help_command(message: Message) -> None:
await send_rich_message(message, HELP_RICH_HTML, "помощи")
await send_rich_message(message, help_view(None), "помощи")