mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-22 20:31:47 +00:00
feat: add button style and emoji support for cabinet mode (Bot API 9.4)
- Upgrade aiogram to 3.25.0 for style/icon_custom_emoji_id support - Add CABINET_BUTTON_STYLE config for global color override - Per-section default styles: subscription (green), balance (blue), referral (green), admin (red), home (blue) - Style priority: explicit > CABINET_BUTTON_STYLE > per-section default - Add icon_custom_emoji_id pass-through for Premium bot owners - Admin panel setting for button style with color picker
This commit is contained in:
@@ -704,6 +704,12 @@ LOGO_FILE=vpn_logo.png
|
||||
# Требует MINIAPP_CUSTOM_URL
|
||||
# Алиасы для обратной совместимости: text, text_only, minimal
|
||||
MAIN_MENU_MODE=default
|
||||
# Стиль кнопок в режиме Cabinet (Bot API 9.4):
|
||||
# primary - синий
|
||||
# success - зелёный
|
||||
# danger - красный
|
||||
# (пустое) - цвета по умолчанию для каждой секции
|
||||
CABINET_BUTTON_STYLE=
|
||||
# Включить управление меню через API (позволяет динамически менять структуру кнопок)
|
||||
MENU_LAYOUT_ENABLED=false
|
||||
|
||||
|
||||
@@ -517,6 +517,8 @@ class Settings(BaseSettings):
|
||||
KASSA_AI_PAYMENT_SYSTEM_ID: int = 44
|
||||
|
||||
MAIN_MENU_MODE: str = 'default' # 'default' | 'cabinet'
|
||||
# Стиль кнопок Cabinet: primary (синий), success (зелёный), danger (красный), '' (по умолчанию для каждой секции)
|
||||
CABINET_BUTTON_STYLE: str = ''
|
||||
CONNECT_BUTTON_MODE: str = 'miniapp_subscription'
|
||||
MINIAPP_CUSTOM_URL: str = ''
|
||||
MINIAPP_STATIC_PATH: str = 'miniapp'
|
||||
|
||||
@@ -375,25 +375,44 @@ def _build_cabinet_main_menu_keyboard(
|
||||
Each button opens the corresponding section of the cabinet frontend
|
||||
via ``MINIAPP_CUSTOM_URL`` + path (e.g. ``/subscription``, ``/balance``).
|
||||
"""
|
||||
from app.utils.miniapp_buttons import build_cabinet_url
|
||||
from app.utils.miniapp_buttons import (
|
||||
CALLBACK_TO_CABINET_STYLE,
|
||||
_resolve_style,
|
||||
build_cabinet_url,
|
||||
)
|
||||
|
||||
def _cabinet_button(text: str, path: str, callback_fallback: str) -> InlineKeyboardButton:
|
||||
global_style = (settings.CABINET_BUTTON_STYLE or '').strip()
|
||||
|
||||
def _cabinet_button(
|
||||
text: str,
|
||||
path: str,
|
||||
callback_fallback: str,
|
||||
*,
|
||||
style: str | None = None,
|
||||
icon_custom_emoji_id: str | None = None,
|
||||
) -> InlineKeyboardButton:
|
||||
url = build_cabinet_url(path)
|
||||
if url:
|
||||
return InlineKeyboardButton(text=text, web_app=types.WebAppInfo(url=url))
|
||||
resolved = _resolve_style(style or global_style or CALLBACK_TO_CABINET_STYLE.get(callback_fallback))
|
||||
return InlineKeyboardButton(
|
||||
text=text,
|
||||
web_app=types.WebAppInfo(url=url),
|
||||
style=resolved,
|
||||
icon_custom_emoji_id=icon_custom_emoji_id or None,
|
||||
)
|
||||
return InlineKeyboardButton(text=text, callback_data=callback_fallback)
|
||||
|
||||
# -- Primary action row: Cabinet home --
|
||||
profile_text = texts.t('MENU_PROFILE', '👤 Личный кабинет')
|
||||
keyboard_rows: list[list[InlineKeyboardButton]] = [
|
||||
[_cabinet_button(profile_text, '/', 'menu_profile_unavailable')],
|
||||
[_cabinet_button(profile_text, '/', 'menu_profile_unavailable', style='primary')],
|
||||
]
|
||||
|
||||
# -- Section buttons as paired rows --
|
||||
paired: list[InlineKeyboardButton] = []
|
||||
|
||||
# Subscription
|
||||
paired.append(_cabinet_button(texts.MENU_SUBSCRIPTION, '/subscription', 'menu_subscription'))
|
||||
# Subscription (green — main action)
|
||||
paired.append(_cabinet_button(texts.MENU_SUBSCRIPTION, '/subscription', 'menu_subscription', style='success'))
|
||||
|
||||
# Balance
|
||||
safe_balance = balance_kopeks or 0
|
||||
@@ -403,11 +422,11 @@ def _build_cabinet_main_menu_keyboard(
|
||||
balance_text = texts.t('BALANCE_BUTTON_DEFAULT', '💰 Баланс: {balance}').format(
|
||||
balance=texts.format_price(safe_balance),
|
||||
)
|
||||
paired.append(_cabinet_button(balance_text, '/balance', 'menu_balance'))
|
||||
paired.append(_cabinet_button(balance_text, '/balance', 'menu_balance', style='primary'))
|
||||
|
||||
# Referrals (if enabled)
|
||||
if settings.is_referral_program_enabled():
|
||||
paired.append(_cabinet_button(texts.MENU_REFERRALS, '/referral', 'menu_referrals'))
|
||||
paired.append(_cabinet_button(texts.MENU_REFERRALS, '/referral', 'menu_referrals', style='success'))
|
||||
|
||||
# Support
|
||||
support_enabled = False
|
||||
@@ -430,7 +449,7 @@ def _build_cabinet_main_menu_keyboard(
|
||||
)
|
||||
)
|
||||
|
||||
# Language selection (stays as callback - not a cabinet section)
|
||||
# Language selection (stays as callback — not a cabinet section)
|
||||
if settings.is_language_selection_enabled():
|
||||
paired.append(InlineKeyboardButton(text=texts.MENU_LANGUAGE, callback_data='menu_language'))
|
||||
|
||||
@@ -443,7 +462,7 @@ def _build_cabinet_main_menu_keyboard(
|
||||
keyboard_rows.append(
|
||||
[
|
||||
InlineKeyboardButton(text=texts.MENU_ADMIN, callback_data='admin_panel'),
|
||||
_cabinet_button('🖥 Веб-Админка', '/admin', 'admin_panel'),
|
||||
_cabinet_button('🖥 Веб-Админка', '/admin', 'admin_panel', style='danger'),
|
||||
]
|
||||
)
|
||||
elif is_moderator:
|
||||
|
||||
@@ -294,6 +294,7 @@ class BotConfigurationService:
|
||||
'LOGO_FILE': 'INTERFACE_BRANDING',
|
||||
'HIDE_SUBSCRIPTION_LINK': 'INTERFACE_SUBSCRIPTION',
|
||||
'MAIN_MENU_MODE': 'INTERFACE',
|
||||
'CABINET_BUTTON_STYLE': 'INTERFACE',
|
||||
'CONNECT_BUTTON_MODE': 'CONNECT_BUTTON',
|
||||
'MINIAPP_CUSTOM_URL': 'CONNECT_BUTTON',
|
||||
'APP_CONFIG_PATH': 'ADDITIONAL',
|
||||
@@ -408,6 +409,12 @@ class BotConfigurationService:
|
||||
ChoiceOption('default', '📋 Полное меню'),
|
||||
ChoiceOption('cabinet', '🏠 Cabinet (МиниАпп)'),
|
||||
],
|
||||
'CABINET_BUTTON_STYLE': [
|
||||
ChoiceOption('', '🎨 По секциям (авто)'),
|
||||
ChoiceOption('primary', '🔵 Синий'),
|
||||
ChoiceOption('success', '🟢 Зелёный'),
|
||||
ChoiceOption('danger', '🔴 Красный'),
|
||||
],
|
||||
'SALES_MODE': [
|
||||
ChoiceOption('classic', '📋 Классический (периоды из .env)'),
|
||||
ChoiceOption('tariffs', '📦 Тарифы (из кабинета)'),
|
||||
|
||||
@@ -27,6 +27,28 @@ CALLBACK_TO_CABINET_PATH: dict[str, str] = {
|
||||
'back_to_menu': '/',
|
||||
}
|
||||
|
||||
# Default button styles per callback_data for cabinet mode.
|
||||
# Values: 'primary' (blue), 'success' (green), 'danger' (red), None (default).
|
||||
CALLBACK_TO_CABINET_STYLE: dict[str, str] = {
|
||||
'menu_balance': 'primary',
|
||||
'balance_topup': 'primary',
|
||||
'menu_subscription': 'success',
|
||||
'subscription': 'success',
|
||||
'subscription_extend': 'success',
|
||||
'subscription_upgrade': 'success',
|
||||
'subscription_connect': 'success',
|
||||
'subscription_resume_checkout': 'success',
|
||||
'return_to_saved_cart': 'success',
|
||||
'menu_buy': 'success',
|
||||
'buy_traffic': 'success',
|
||||
'menu_referrals': 'success',
|
||||
'menu_referral': 'success',
|
||||
'menu_support': 'primary',
|
||||
'menu_info': 'primary',
|
||||
'menu_profile': 'primary',
|
||||
'back_to_menu': 'primary',
|
||||
}
|
||||
|
||||
# Mapping from broadcast button keys to cabinet paths.
|
||||
BUTTON_KEY_TO_CABINET_PATH: dict[str, str] = {
|
||||
'balance': '/balance/top-up',
|
||||
@@ -38,6 +60,16 @@ BUTTON_KEY_TO_CABINET_PATH: dict[str, str] = {
|
||||
'home': '/',
|
||||
}
|
||||
|
||||
# Valid style values accepted by the Telegram Bot API.
|
||||
_VALID_STYLES = frozenset({'primary', 'success', 'danger'})
|
||||
|
||||
|
||||
def _resolve_style(style: str | None) -> str | None:
|
||||
"""Return a validated style or ``None``."""
|
||||
if style and style in _VALID_STYLES:
|
||||
return style
|
||||
return None
|
||||
|
||||
|
||||
def build_cabinet_url(path: str = '') -> str:
|
||||
"""Join ``MINIAPP_CUSTOM_URL`` with an optional *path* segment.
|
||||
@@ -66,6 +98,8 @@ def build_miniapp_or_callback_button(
|
||||
*,
|
||||
callback_data: str,
|
||||
cabinet_path: str | None = None,
|
||||
style: str | None = None,
|
||||
icon_custom_emoji_id: str | None = None,
|
||||
) -> InlineKeyboardButton:
|
||||
"""Create a button that opens the cabinet miniapp or falls back to a callback.
|
||||
|
||||
@@ -74,6 +108,13 @@ def build_miniapp_or_callback_button(
|
||||
by ``cabinet_path`` (explicit) or inferred from ``callback_data`` via
|
||||
``CALLBACK_TO_CABINET_PATH``.
|
||||
|
||||
Button styling (Bot API 9.4):
|
||||
- ``style`` overrides the button color: ``'primary'`` (blue),
|
||||
``'success'`` (green), ``'danger'`` (red). When omitted the style is
|
||||
resolved from ``CABINET_BUTTON_STYLE`` config or per-section defaults.
|
||||
- ``icon_custom_emoji_id`` shows a custom emoji before the button text
|
||||
(requires bot owner to have Telegram Premium).
|
||||
|
||||
When ``callback_data`` is not found in the mapping and no explicit
|
||||
``cabinet_path`` is given, the button falls back to a regular Telegram
|
||||
callback — this keeps actions like ``claim_discount_*`` working correctly.
|
||||
@@ -88,9 +129,17 @@ def build_miniapp_or_callback_button(
|
||||
if path:
|
||||
url = build_cabinet_url(path)
|
||||
if url:
|
||||
# Resolve style: explicit > global config > per-section default
|
||||
resolved_style = _resolve_style(
|
||||
style
|
||||
or (settings.CABINET_BUTTON_STYLE or '').strip()
|
||||
or CALLBACK_TO_CABINET_STYLE.get(callback_data)
|
||||
)
|
||||
return InlineKeyboardButton(
|
||||
text=text,
|
||||
web_app=types.WebAppInfo(url=url),
|
||||
style=resolved_style,
|
||||
icon_custom_emoji_id=icon_custom_emoji_id or None,
|
||||
)
|
||||
|
||||
return InlineKeyboardButton(text=text, callback_data=callback_data)
|
||||
|
||||
@@ -6,7 +6,7 @@ readme = 'README.md'
|
||||
license = { text = 'MIT' }
|
||||
requires-python = '==3.13.*'
|
||||
dependencies = [
|
||||
'aiogram>=3.22.0',
|
||||
"aiogram>=3.25.0",
|
||||
'sqlalchemy>=2.0.43',
|
||||
'alembic>=1.16.5',
|
||||
'asyncpg>=0.30.0',
|
||||
|
||||
10
uv.lock
generated
10
uv.lock
generated
@@ -13,7 +13,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiogram"
|
||||
version = "3.24.0"
|
||||
version = "3.25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
@@ -23,9 +23,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fd/2f/04f47e81def8f2168679b1551e665e7ee02cf063e7bddace9fb5d1ce2f35/aiogram-3.24.0.tar.gz", hash = "sha256:ec547ede5bfa8a7a4f5fb02c75391333fc43b6f3de6a6d3f00a32e27628df5f6", size = 1713321, upload-time = "2026-01-02T00:56:55.3Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/31/360c4ce76e60e9e7bcdda1af1ab4331d78837fbb22847a62121ad32b7672/aiogram-3.25.0.tar.gz", hash = "sha256:8a8b0c34f8c4ca8a6501b954abb0eeba26743449e35e20b70c0d810347354c3c", size = 1721010, upload-time = "2026-02-10T21:50:25.473Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a5/7ba5f75b56f87a956b9e5a3e823bcbb5b55fc968914a16f3c7aa659cfc89/aiogram-3.24.0-py3-none-any.whl", hash = "sha256:eb3cc05b0ec53c7e24d7eada5c069aee2f431332e2e7bc2c8adf30d13b02f715", size = 706866, upload-time = "2026-01-02T00:56:53.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/be/1090252415e192687985517162dbdcee2ec4150cda1fa52bf57ae1f1c2a8/aiogram-3.25.0-py3-none-any.whl", hash = "sha256:0243966e93fbde14e90c0dfd0b3776c637ebf7ddcca2c7ee81ecbd68d9490cce", size = 713972, upload-time = "2026-02-10T21:50:23.253Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1114,7 +1114,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "remnawave-bedolaga-telegram-bot"
|
||||
version = "3.8.0"
|
||||
version = "3.10.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiogram" },
|
||||
@@ -1145,7 +1145,7 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiogram", specifier = ">=3.22.0" },
|
||||
{ name = "aiogram", specifier = ">=3.25.0" },
|
||||
{ name = "aiosqlite", specifier = ">=0.21.0" },
|
||||
{ name = "alembic", specifier = ">=1.16.5" },
|
||||
{ name = "asyncpg", specifier = ">=0.30.0" },
|
||||
|
||||
Reference in New Issue
Block a user