feat: move tg bot interaction keyboard to separate command, show only web app and support buttons by default

This commit is contained in:
3252a8
2026-04-24 12:43:21 +03:00
parent c08ac854b2
commit 9a84be85c6
5 changed files with 166 additions and 36 deletions
+109 -12
View File
@@ -14,6 +14,7 @@ from db.models import User
from bot.keyboards.inline.user_keyboards import (
get_main_menu_inline_keyboard,
get_bot_interface_inline_keyboard,
get_language_selection_keyboard,
get_channel_subscription_keyboard,
get_information_links_keyboard,
@@ -29,6 +30,25 @@ from bot.utils.callback_answer import safe_answer_callback
router = Router(name="user_start_router")
async def should_show_trial_button(
settings: Settings,
subscription_service: SubscriptionService,
session: AsyncSession,
user_id: int) -> bool:
if not settings.TRIAL_ENABLED:
return False
if hasattr(subscription_service, 'has_had_any_subscription') and callable(
getattr(subscription_service, 'has_had_any_subscription')):
return not await subscription_service.has_had_any_subscription(
session, user_id)
logging.error(
"Method has_had_any_subscription is missing in SubscriptionService!"
)
return False
async def send_main_menu(target_event: Union[types.Message,
types.CallbackQuery],
settings: Settings,
@@ -61,18 +81,8 @@ async def send_main_menu(target_event: Union[types.Message,
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
show_trial_button_in_menu = False
if settings.TRIAL_ENABLED:
if hasattr(
subscription_service, 'has_had_any_subscription') and callable(
getattr(subscription_service, 'has_had_any_subscription')):
if not await subscription_service.has_had_any_subscription(
session, user_id):
show_trial_button_in_menu = True
else:
logging.error(
"Method has_had_any_subscription is missing in SubscriptionService for send_main_menu!"
)
show_trial_button_in_menu = await should_show_trial_button(
settings, subscription_service, session, user_id)
text = _(key="main_menu_greeting", user_name=user_full_name)
reply_markup = get_main_menu_inline_keyboard(current_lang, i18n, settings,
@@ -123,6 +133,68 @@ async def send_main_menu(target_event: Union[types.Message,
)
async def send_bot_interface_menu(
target_event: Union[types.Message, types.CallbackQuery],
settings: Settings,
i18n_data: dict,
subscription_service: SubscriptionService,
session: AsyncSession,
is_edit: bool = False):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n:
logging.error("i18n_instance missing in send_bot_interface_menu.")
return
user_id = target_event.from_user.id
show_trial_button_in_menu = await should_show_trial_button(
settings, subscription_service, session, user_id)
text = i18n.gettext(current_lang, "bot_interface_menu_title")
reply_markup = get_bot_interface_inline_keyboard(
current_lang, i18n, settings, show_trial_button_in_menu)
target_message_obj: Optional[types.Message] = None
if isinstance(target_event, types.Message):
target_message_obj = target_event
elif isinstance(target_event, types.CallbackQuery) and target_event.message:
target_message_obj = target_event.message
if not target_message_obj:
logging.error(
"send_bot_interface_menu: target_message_obj is None for user %s.",
user_id,
)
return
try:
if is_edit:
await target_message_obj.edit_text(text, reply_markup=reply_markup)
else:
await target_message_obj.answer(text, reply_markup=reply_markup)
if isinstance(target_event, types.CallbackQuery):
await safe_answer_callback(target_event)
except Exception as e_send_edit:
logging.warning(
"Failed to send/edit bot interface menu (user: %s, is_edit: %s): %s - %s.",
user_id,
is_edit,
type(e_send_edit).__name__,
e_send_edit,
)
if is_edit:
try:
await target_message_obj.answer(text, reply_markup=reply_markup)
except Exception as e_send_new:
logging.error(
"Also failed to send new bot interface menu for user %s: %s",
user_id,
e_send_new,
)
async def ensure_required_channel_subscription(
event: Union[types.Message, types.CallbackQuery],
settings: Settings,
@@ -640,6 +712,31 @@ async def start_command_handler(message: types.Message,
is_edit=False)
@router.message(Command("tg"))
async def tg_interface_command_handler(message: types.Message,
state: FSMContext,
settings: Settings,
i18n_data: dict,
subscription_service: SubscriptionService,
session: AsyncSession):
await state.clear()
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
db_user = await user_dal.get_user_by_id(session, message.from_user.id)
if not await ensure_required_channel_subscription(message, settings, i18n,
current_lang, session,
db_user):
return
await send_bot_interface_menu(message,
settings,
i18n_data,
subscription_service,
session,
is_edit=False)
@router.callback_query(F.data == "channel_subscription:verify")
async def verify_channel_subscription_callback(
callback: types.CallbackQuery,
+36 -13
View File
@@ -13,6 +13,37 @@ def get_main_menu_inline_keyboard(
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder()
if settings.SUBSCRIPTION_MINI_APP_URL:
builder.row(
InlineKeyboardButton(
text=_(key="menu_personal_account_button"),
web_app=WebAppInfo(url=settings.SUBSCRIPTION_MINI_APP_URL),
)
)
else:
builder.row(
InlineKeyboardButton(
text=_(key="menu_personal_account_button"),
callback_data="main_action:my_subscription",
)
)
if settings.SUPPORT_LINK:
builder.row(
InlineKeyboardButton(text=_(key="menu_support_button"),
url=settings.SUPPORT_LINK))
return builder.as_markup()
def get_bot_interface_inline_keyboard(
lang: str,
i18n_instance,
settings: Settings,
show_trial_button: bool = False) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder()
if show_trial_button and settings.TRIAL_ENABLED:
builder.row(
InlineKeyboardButton(text=_(key="menu_activate_trial_button"),
@@ -21,20 +52,12 @@ def get_main_menu_inline_keyboard(
builder.row(
InlineKeyboardButton(text=_(key="menu_subscribe_inline"),
callback_data="main_action:subscribe"))
if settings.SUBSCRIPTION_MINI_APP_URL:
builder.row(
InlineKeyboardButton(
text=_(key="menu_my_subscription_inline"),
web_app=WebAppInfo(url=settings.SUBSCRIPTION_MINI_APP_URL),
)
)
else:
builder.row(
InlineKeyboardButton(
text=_(key="menu_my_subscription_inline"),
callback_data="main_action:my_subscription",
)
builder.row(
InlineKeyboardButton(
text=_(key="menu_my_subscription_inline"),
callback_data="main_action:my_subscription",
)
)
referral_button = InlineKeyboardButton(
text=_(key="menu_referral_inline"),
+15 -9
View File
@@ -115,7 +115,7 @@ async def on_startup_configured(dispatcher: Dispatcher):
try:
menu_text = i18n_instance.gettext(
settings.DEFAULT_LANGUAGE,
"menu_my_subscription_inline",
"menu_personal_account_button",
)
await bot.set_chat_menu_button(
menu_button=MenuButtonWebApp(
@@ -132,14 +132,20 @@ async def on_startup_configured(dispatcher: Dispatcher):
f"STARTUP: Failed to register mini app domain: {e}", exc_info=True
)
if settings.START_COMMAND_DESCRIPTION:
try:
await bot.set_my_commands([
BotCommand(command="start", description=settings.START_COMMAND_DESCRIPTION)
])
logging.info("STARTUP: /start command description set.")
except Exception as e:
logging.error(f"STARTUP: Failed to set bot commands: {e}", exc_info=True)
try:
bot_commands = [
BotCommand(command="tg", description="Интерфейс в боте"),
]
if settings.START_COMMAND_DESCRIPTION:
bot_commands.insert(
0,
BotCommand(command="start",
description=settings.START_COMMAND_DESCRIPTION),
)
await bot.set_my_commands(bot_commands)
logging.info("STARTUP: bot command descriptions set.")
except Exception as e:
logging.error(f"STARTUP: Failed to set bot commands: {e}", exc_info=True)
# Initialize message queue manager
try:
+3 -1
View File
@@ -7,9 +7,10 @@
"channel_subscription_verified_success": "✅ Subscription confirmed! You're good to go.",
"webapp_auth_success": "✅ Login confirmed. Return to the Web App.",
"webapp_auth_expired": "The login link has expired. Return to the Web App and try again.",
"main_menu_greeting": "Hi, {user_name}! 👋\nWhat would you like to do?\n\nTo set up the connection on your device, go to \"My subscription\" and click \"Connect\". If needed, add access days in the \"Buy\" section.",
"main_menu_greeting": "Hi, {user_name}! 👋\nOpen \"Personal Account\" to set up the connection, view your subscription, or renew access.",
"menu_activate_trial_button": "🆓 Free Trial",
"menu_subscribe_inline": "🚀 Purchase",
"menu_personal_account_button": "🔑 Personal Account",
"menu_my_subscription_inline": "🔐 My Subscription",
"no_subscription_options_available": "Subscription issuance is not configured by the bot administrator",
"menu_referral_inline": "🎁 Invite friend",
@@ -20,6 +21,7 @@
"menu_support_button": "💬 Support",
"menu_terms_button": "📄 Terms of Service",
"menu_info_button": "️ Information",
"bot_interface_menu_title": "Bot interface",
"info_links_message": "Choose a document:",
"privacy_policy_button": "🔒 Privacy Policy",
"user_agreement_button": "📄 User Agreement",
+3 -1
View File
@@ -7,9 +7,10 @@
"channel_subscription_verified_success": "✅ Подписка подтверждена! Можно продолжать.",
"webapp_auth_success": "✅ Вход подтвержден. Вернитесь в Web App.",
"webapp_auth_expired": "Ссылка авторизации устарела. Вернитесь в Web App и попробуйте еще раз.",
"main_menu_greeting": "Привет, {user_name}! 👋\nЧто бы вы хотели сделать?\n\nДля настройки подключения на устройстве перейдите в раздел \"Моя подписка\" и нажмите кнопку \"Подключиться\". При необходимости добавьте дни доступа в разделе \"Купить\".",
"main_menu_greeting": "Привет, {user_name}! 👋\nПерейдите в \"Личный кабинет\", чтобы настроить подключение, посмотреть подписку или продлить доступ.",
"menu_activate_trial_button": "🆓 Пробный период",
"menu_subscribe_inline": "🚀 Купить",
"menu_personal_account_button": "🔑 Личный кабинет",
"menu_my_subscription_inline": "🔐 Моя подписка",
"no_subscription_options_available": "Выдача подписки не настроена администратором бота",
"menu_referral_inline": "🎁 Пригласить друга",
@@ -20,6 +21,7 @@
"menu_support_button": "💬 Поддержка",
"menu_terms_button": "📄 Условия сервиса",
"menu_info_button": "ℹ️ Информация",
"bot_interface_menu_title": "Интерфейс в боте",
"info_links_message": "Выберите документ:",
"privacy_policy_button": "🔒 Политика конфиденциальности",
"user_agreement_button": "📄 Пользовательское соглашение",