feat: reply-клавиатура + фикс пропавший _send_idea
- keyboards.py: MAIN_MENU (✍️ Написать пост / 💡 Идея / 🎨 Мой стиль / 📖 Помощь / 💳 Подписка / 👤 Баланс) и LOCKED_MENU — при блокировке первой кнопкой идёт «💳 Оплатить подписку» - кнопки не обходят роутинг: resolve_button_text превращает их в slash-команду (/help, /balance, /subscribe, /style) или готовый запрос («Напиши пост», «предложи идею…») - новая команда /style показывает сохранённый Tone of Voice и подсказывает, как его настроить - set_my_commands публикует slash-команды при старте, ошибка не роняет бот - fix: _send_idea вызывалась на bot.py, но была потеряна в коммите про подписку — /idea падал с NameError; функция восстановлена - 17 новых тестов клавиатуры; 92 теста OK; VERSION 0.1.0+26
This commit is contained in:
1 parent
3c8917c20a
commit
971bf115da
7 files changed
+312
-11
No files matched your search
@@ -12,13 +12,15 @@ 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 CallbackQuery, InputRichMessage, Message
|
||||
from aiogram.types import BotCommand, CallbackQuery, InputRichMessage, Message
|
||||
|
||||
import keyboards
|
||||
import payments
|
||||
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 WELCOME_RICH_HTML, help_plain_view, help_view, rich_to_plain
|
||||
from keyboards import LOCKED_MENU, MAIN_MENU, resolve_button_text
|
||||
from payments import send_subscription_invoice
|
||||
from user_store import (
|
||||
add_post_samples,
|
||||
@@ -27,6 +29,7 @@ from user_store import (
|
||||
get_deploy_notice_version,
|
||||
get_idea_history,
|
||||
get_post_history,
|
||||
get_post_samples,
|
||||
get_recent_post_history_text,
|
||||
get_tone_of_voice,
|
||||
init_db,
|
||||
@@ -166,14 +169,19 @@ async def start(message: Message) -> None:
|
||||
return
|
||||
await notify_development_deploy(message.bot)
|
||||
await asyncio.to_thread(ensure_subscription, message.from_user.id)
|
||||
await message.answer("Меню — внизу экрана. По нему можно написать пост, взять идею или открыть справку.")
|
||||
await send_rich_message(message, WELCOME_RICH_HTML, "приветствия")
|
||||
await asyncio.sleep(0.8)
|
||||
await help_command(message)
|
||||
await message.answer("Меню обновлено.", reply_markup=MAIN_MENU)
|
||||
|
||||
|
||||
async def _show_subscription_info(message: Message, sub: sqlite3.Row | None) -> None:
|
||||
if not sub:
|
||||
await message.answer("У тебя нет активной подписки или триала. Начни с команды /subscribe.")
|
||||
await message.answer(
|
||||
"У тебя нет активной подписки или триала. Начни с команды /subscribe.",
|
||||
reply_markup=LOCKED_MENU,
|
||||
)
|
||||
return
|
||||
days_left = user_store._days_until(sub)
|
||||
if days_left >= 0:
|
||||
@@ -190,7 +198,8 @@ async def _show_subscription_info(message: Message, sub: sqlite3.Row | None) ->
|
||||
else:
|
||||
await message.answer(
|
||||
"Твой период (триал или подписка) истёк. "
|
||||
"Оплати /subscribe, чтобы продолжить. Стоимость — 100 звёзд в месяц."
|
||||
"Оплати /subscribe, чтобы продолжить. Стоимость — 100 звёзд в месяц.",
|
||||
reply_markup=LOCKED_MENU,
|
||||
)
|
||||
if sub["last_paid_at"] is None:
|
||||
await message.answer(
|
||||
@@ -230,6 +239,28 @@ async def subscribe_command(message: Message) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def style_command(message: Message) -> None:
|
||||
if message.from_user is None:
|
||||
await message.answer("Не удалось определить пользователя.")
|
||||
return
|
||||
tone = await asyncio.to_thread(get_tone_of_voice, message.from_user.id)
|
||||
samples = await asyncio.to_thread(get_post_samples, message.from_user.id)
|
||||
if not tone and not samples:
|
||||
await message.answer(
|
||||
"Пока не настроен. Пришли скриншоты своих опубликованных постов — "
|
||||
"я распознаю текст, сохраню образцы и построю правила твоего тона.",
|
||||
reply_markup=MAIN_MENU,
|
||||
)
|
||||
return
|
||||
await message.answer(
|
||||
f"<b>Твой стиль</b>\n\n{tone or 'Правила пока не сформированы.'}",
|
||||
parse_mode="HTML",
|
||||
reply_markup=MAIN_MENU,
|
||||
)
|
||||
if samples:
|
||||
await message.answer(f"Образцов сохранено: {len(samples)}.")
|
||||
|
||||
|
||||
async def clear_command(message: Message) -> None:
|
||||
if message.from_user is None:
|
||||
await message.answer("Не удалось определить пользователя.")
|
||||
@@ -262,6 +293,11 @@ async def _check_subscription(message: Message) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def _send_idea(message: Message, idea: dict[str, str], user_id: int) -> None:
|
||||
await message.answer(f"{idea['title']}\n\n{idea['angle']}\n\nЗачем: {idea['why']}")
|
||||
await asyncio.to_thread(save_idea, user_id, idea)
|
||||
|
||||
|
||||
async def _send_three_posts(message: Message, posts: list[str], user_id: int | None = None, origin: str = "generated") -> None:
|
||||
for post in posts:
|
||||
await message.answer(post)
|
||||
@@ -498,19 +534,27 @@ async def transcribe_voice(message: Message, bot: Bot) -> None:
|
||||
|
||||
@router.message(F.text & F.text.startswith("/"))
|
||||
async def handle_command_text(message: Message) -> None:
|
||||
command = (message.text or "").strip().split(maxsplit=1)[0].split("@", 1)[0].casefold()
|
||||
raw = (message.text or "").strip()
|
||||
# Кнопки reply-клавиатуры отправляют текст, который мапится на команду/запрос.
|
||||
resolved = resolve_button_text(raw) or raw
|
||||
command = resolved.split(maxsplit=1)[0].split("@", 1)[0].casefold()
|
||||
if command == "/help":
|
||||
await help_command(message)
|
||||
elif command == "/start":
|
||||
await start(message)
|
||||
elif command == "/clear":
|
||||
await clear_command(message)
|
||||
elif command == "/style":
|
||||
await style_command(message)
|
||||
elif command == "/idea":
|
||||
await _classify_and_respond(message, "предложи идею для нового поста")
|
||||
elif command == "/balance":
|
||||
await balance_command(message)
|
||||
elif command == "/subscribe":
|
||||
await subscribe_command(message)
|
||||
elif resolved != raw:
|
||||
# Это была кнопка меню, а не команда — отправляем её как обычный запрос.
|
||||
await _classify_and_respond(message, resolved)
|
||||
else:
|
||||
await message.answer("Неизвестная команда. Используй /help.")
|
||||
|
||||
@@ -518,8 +562,33 @@ async def handle_command_text(message: Message) -> None:
|
||||
@router.message(F.text & ~F.text.startswith("/"))
|
||||
async def handle_text(message: Message) -> None:
|
||||
await notify_development_deploy(message.bot)
|
||||
if message.text:
|
||||
await _classify_and_respond(message, message.text)
|
||||
text = (message.text or "").strip()
|
||||
if not text:
|
||||
return
|
||||
button = resolve_button_text(text)
|
||||
if button:
|
||||
if button.startswith("/"):
|
||||
await handle_command_text(message)
|
||||
return
|
||||
text = button
|
||||
await _classify_and_respond(message, text)
|
||||
|
||||
|
||||
async def set_main_menu(bot: Bot) -> None:
|
||||
"""Публикует reply-клавиатуру для всех пользователей бота."""
|
||||
try:
|
||||
await bot.set_my_commands(
|
||||
commands=[
|
||||
BotCommand(command="help", description="Справка по боту"),
|
||||
BotCommand(command="style", description="Мой Tone of Voice"),
|
||||
BotCommand(command="idea", description="Идея для поста"),
|
||||
BotCommand(command="balance", description="Статус подписки"),
|
||||
BotCommand(command="subscribe", description="Оплатить подписку"),
|
||||
BotCommand(command="clear", description="Сбросить мои данные"),
|
||||
]
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Bot commands publish failed (%s): %s", type(exc).__name__, str(exc)[:120])
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -536,6 +605,8 @@ async def main() -> None:
|
||||
dp = Dispatcher()
|
||||
dp.include_router(payments.router)
|
||||
dp.include_router(router)
|
||||
# Ставим меню один раз при старте, чтобы оно было у всех, кто писал боту.
|
||||
await set_main_menu(bot)
|
||||
|
||||
try:
|
||||
await dp.start_polling(bot)
|
||||
|
||||
Reference in new issue
Block a user