Files
social-post-writer-bot/help_message.py
T
admin c85f0a34fe 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
2026-10-06 23:15:47 +02:00

127 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import html
WELCOME_RICH_HTML = """<h1>Привет! Я — бот для написания постов</h1>
<p>Помогаю писать и редактировать посты для соцсетей. Расскажи, о чём нужен пост, а я верну три варианта: короткий, развёрнутый и с призывом к действию.</p>
<h2>Примеры запросов</h2>
<ul>
<li>«Напиши пост про запуск нового продукта»</li>
<li>«Сделай пост-напоминание о вебинаре в официальном тоне»</li>
<li>«Обнови этот вариант, добавь эмодзи» — реплаем на мой текст</li>
</ul>
<p>Также я умею запоминать твой стиль: пришли скриншоты своих постов. Хочешь идею — напиши «предложи тему».</p>
<p>Ниже — подробная инструкция по работе с ботом.</p>
"""
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>""",
),
"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>""",
),
"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:
import re
text = re.sub(r"<li(?:\s[^>]*)?>", "• ", rich_html, flags=re.IGNORECASE)
text = re.sub(r"</?(?:h[1-6]|p|ul|ol|li|hr|table|tr|td|th|input)(?:\s[^>]*)?/?>", "\n", text, flags=re.IGNORECASE)
text = re.sub(r"<[^>]+>", "", text)
text = html.unescape(text)
return "\n".join(line.strip() for line in text.splitlines() if line.strip())