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

+94 -16
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import html
WELCOME_RICH_HTML = """<h1>Привет! Я — бот для написания постов</h1>
<p>Помогаю писать и редактировать посты для соцсетей. Расскажи, о чём нужен пост, а я верну три варианта: короткий, развёрнутый и с призывом к действию.</p>
<h2>Примеры запросов</h2>
@@ -12,30 +13,107 @@ WELCOME_RICH_HTML = """<h1>Привет! Я — бот для написания
<p>Также я умею запоминать твой стиль: пришли скриншоты своих постов. Хочешь идею — напиши «предложи тему».</p>
<p>Ниже — подробная инструкция по работе с ботом.</p>
"""
HELP_RICH_HTML = """<h1>Помощь по боту</h1>
<p>Я помогаю писать и редактировать посты для социальных сетей. Генерирую посты только по явной просьбе.</p>
<hr/>
<h2>Написать пост</h2>
<ol>
HELP_MENU_TITLE = "Помощь по боту"
HELP_MENU_INTRO = "Выбери раздел — откроется только он."
HELP_SECTIONS: dict[str, tuple[str, str]] = {
"posts": (
"✍️ Написать пост",
"""<ol>
<li>Напиши сообщение или отправь голосовое с прямой просьбой, например: «Придумай пост для Telegram про запуск нового продукта».</li>
<li>Можешь указать площадку, аудиторию, факты, длину, цель и другие требования. Можно надиктовать подробное ТЗ или просто тему.</li>
<li>Я распознаю речь, проверю, просишь ли ты именно написать пост, и верну три варианта отдельными сообщениями.</li>
</ol>
<p>В сообщениях с вариантами будет только текст поста: без нумерации и вступлений, с абзацами, переносами строк и уместными эмодзи.</p>
<h2>Настроить Tone of Voice</h2>
<p>Пришли скриншот или галерею скриншотов своих опубликованных постов. Я распознаю текст постов и сохраню его как образцы, дополню предыдущие примеры и обновлю только правила Tone of Voice. Сами публикации не переписываю.</p>
<h2>Попросить правку</h2>
<p>Ответь реплаем на сообщение с вариантом и укажи, что изменить. Например: «Сделай короче и добавь призыв». Я учту оригинальный текст и верну три новых варианта отдельными сообщениями.</p>
<h2>Подписка</h2>
<p>У тебя есть пробный период — 3 дня бесплатно. Дальше — подписка на 30 дней за 100 звёзд.</p>
<p>В сообщениях с вариантами будет только текст поста: без нумерации и вступлений, с абзацами, переносами строк и уместными эмодзи.</p>""",
),
"tone": (
"🎨 Tone of Voice",
"""<p>Пришли скриншот или галерею скриншотов своих опубликованных постов. Я распознаю текст постов и сохраню его как образцы, дополню предыдущие примеры и обновлю только правила Tone of Voice. Сами публикации не переписываю.</p>""",
),
"edit": (
"✏️ Правка поста",
"""<p>Ответь реплаем на сообщение с вариантом и укажи, что изменить. Например: «Сделай короче и добавь призыв». Я учту оригинальный текст и верну три новых варианта отдельными сообщениями.</p>""",
),
"idea": (
"💡 Идея для поста",
"""<p>Напиши «предложи идею для поста», «о чём написать?» или «накидай тему» — верну одну идею. Команда <code>/idea</code> работает так же.</p>
<p>Идеи сохраняются, чтобы не повторяться: каждая новая идея отличается от предыдущих.</p>""",
),
"subscription": (
"💳 Подписка",
"""<p>У тебя есть пробный период — 3 дня бесплатно. Дальше — подписка на 30 дней за 100 звёзд.</p>
<ul>
<li><code>/balance</code> — узнать статус: сколько осталось дней триала или подписки.</li>
<li><code>/subscribe</code> — оплатить подписку на 30 дней.</li>
</ul>
<p>Когда период закончится, генерация постов и идей станет недоступна — бот предложит оформить подписку. Команды <code>/help</code>, <code>/balance</code>, <code>/subscribe</code>, <code>/start</code> и <code>/clear</code> работают всегда.</p>
<h2>Команды</h2>
<p>Быстрые slash-команды только на английском: <code>/help</code> — эта инструкция; <code>/start</code> — краткое описание бота; <code>/clear</code> — полностью обнулить твои данные бота, кроме технических API-логов; <code>/idea</code> — предложить одну идею для нового поста; <code>/balance</code> — статус подписки; <code>/subscribe</code> — оплатить подписку на 30 дней. Можно попросить идею обычными словами: «предложи идею для поста», «о чём написать?», «накидай тему».</p>
"""
<p>Когда период закончится, генерация постов и идей станет недоступна — бот предложит оформить подписку. Команды <code>/help</code>, <code>/balance</code>, <code>/subscribe</code>, <code>/start</code> и <code>/clear</code> работают всегда.</p>""",
),
"commands": (
"⌨️ Команды",
"""<p>Быстрые slash-команды только на английском: <code>/help</code> — эта справка; <code>/start</code> — краткое описание бота; <code>/clear</code> — полностью обнулить твои данные бота, кроме технических API-логов; <code>/idea</code> — предложить одну идею для нового поста; <code>/balance</code> — статус подписки; <code>/subscribe</code> — оплатить подписку на 30 дней.</p>
<p>Можно попросить идею обычными словами: «предложи идею для поста», «о чём написать?», «накидай тему».</p>""",
),
}
HELP_SECTION_ORDER: tuple[str, ...] = tuple(HELP_SECTIONS)
HELP_BACK = "⬅️ В меню"
HELP_PREV = "⬅️ Предыдущий"
HELP_NEXT = "➡️ Следующий"
def _section_nav(section: str) -> str:
buttons = [f'<tg-button type="callback_data" data="help:menu" style="primary">{HELP_BACK}</tg-button>']
index = HELP_SECTION_ORDER.index(section)
if index > 0:
previous = HELP_SECTION_ORDER[index - 1]
buttons.append(
f'<tg-button type="callback_data" data="help:{previous}">{HELP_PREV}</tg-button>'
)
if index + 1 < len(HELP_SECTION_ORDER):
following = HELP_SECTION_ORDER[index + 1]
buttons.append(
f'<tg-button type="callback_data" data="help:{following}">{HELP_NEXT}</tg-button>'
)
return '<tg-button-row align="center">' + "".join(buttons) + "</tg-button-row>"
def help_menu_html() -> str:
buttons = [
f'<tg-button type="callback_data" data="help:{section}" style="primary">'
f"{HELP_SECTIONS[section][0]}</tg-button>"
for section in HELP_SECTION_ORDER
]
pairs = "".join(
'<tg-button-row align="center">' + "".join(buttons[i : i + 2]) + "</tg-button-row>"
for i in range(0, len(buttons), 2)
)
return (
f"<h1>{HELP_MENU_TITLE}</h1>"
f"<p>{HELP_MENU_INTRO}</p>"
f"<p>Я помогаю писать и редактировать посты для соцсетей. "
f"Генерирую посты только по явной просьбе.</p>"
f"{pairs}"
)
def help_section_html(section: str) -> str:
title, body = HELP_SECTIONS[section]
return f"<h1>{title}</h1>{body}{_section_nav(section)}"
def help_view(section: str | None = None) -> str:
if section and section in HELP_SECTIONS:
return help_section_html(section)
return help_menu_html()
def help_plain_view(section: str | None = None) -> str:
if section and section in HELP_SECTIONS:
title, body = HELP_SECTIONS[section]
return f"{HELP_MENU_TITLE} — {title}\n\n{rich_to_plain(body)}"
lines = [HELP_MENU_TITLE, HELP_MENU_INTRO]
lines += [f"• {HELP_SECTIONS[name][0]}" for name in HELP_SECTION_ORDER]
return "\n".join(lines)
def rich_to_plain(rich_html: str) -> str: