Merge branch 'dev' into feature/new-tariffs
This commit is contained in:
@@ -315,7 +315,9 @@ Web App запускается в том же контейнере, что и б
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
После этого кнопка «Моя подписка» в меню бота откроет Web App. Web App показывает текущую ссылку подключения, остаток времени, трафик, оплату и блок аккаунта. Пользователь может привязать email к Telegram-аккаунту через код из письма или привязать Telegram к email-аккаунту через Telegram OAuth Login. После привязки вход работает обоими способами.
|
||||
После этого кнопка «Личный кабинет» в меню бота откроет Web App. Рядом доступна кнопка «Бот-меню» для открытия расширенного интерфейса в чате без команды `/tg`, но основной сценарий управления подпиской удобнее проходить в личном кабинете. Web App показывает текущую ссылку подключения, остаток времени, трафик, оплату и блок аккаунта. Пользователь может привязать email к Telegram-аккаунту через код из письма или привязать Telegram к email-аккаунту через Telegram OAuth Login. После привязки вход работает обоими способами.
|
||||
|
||||
Реферальные ссылки доступны в двух форматах: Telegram deep-link `https://t.me/<bot>?start=ref_u<code>` и Web App ссылка с query-параметром `ref=u<code>`. Web App учитывает `ref`, `start`, `start_param` и Telegram Mini Apps `start_param`, сохраняет найденный параметр до авторизации и передаёт его в Telegram OAuth и email-вход, чтобы регистрация корректно привязалась к пригласившему.
|
||||
|
||||
Для email-регистраций пользователь в панели Remnawave создается с анонимным username вида `em_<referral_code>`; email добавляется в описание пользователя панели и, если API панели принимает поле email, передается отдельным полем. Для Telegram-регистраций сохраняется существующая схема `tg_<telegram_id>`.
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ MAX_PROMO_CODE_INPUT_LENGTH = 100
|
||||
|
||||
async def prompt_promo_code_input(callback: types.CallbackQuery,
|
||||
state: FSMContext, i18n_data: dict,
|
||||
settings: Settings, session: AsyncSession):
|
||||
settings: Settings, session: AsyncSession,
|
||||
back_callback: str = "main_action:back_to_main"):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
if not i18n:
|
||||
@@ -52,14 +53,22 @@ async def prompt_promo_code_input(callback: types.CallbackQuery,
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text=_(key="promo_code_prompt"),
|
||||
reply_markup=get_back_to_main_menu_markup(current_lang, i18n))
|
||||
reply_markup=get_back_to_main_menu_markup(
|
||||
current_lang,
|
||||
i18n,
|
||||
callback_data=back_callback,
|
||||
))
|
||||
except Exception as e_edit:
|
||||
logging.warning(
|
||||
f"Failed to edit message for promo prompt: {e_edit}. Sending new one."
|
||||
)
|
||||
await callback.message.answer(
|
||||
text=_(key="promo_code_prompt"),
|
||||
reply_markup=get_back_to_main_menu_markup(current_lang, i18n))
|
||||
reply_markup=get_back_to_main_menu_markup(
|
||||
current_lang,
|
||||
i18n,
|
||||
callback_data=back_callback,
|
||||
))
|
||||
|
||||
await safe_answer_callback(callback)
|
||||
await state.set_state(UserPromoStates.waiting_for_promo_code)
|
||||
|
||||
@@ -9,17 +9,16 @@ from config.settings import Settings
|
||||
from db.dal import user_dal
|
||||
from bot.services.referral_service import ReferralService
|
||||
|
||||
from bot.keyboards.inline.user_keyboards import get_back_to_main_menu_markup
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
router = Router(name="user_referral_router")
|
||||
|
||||
|
||||
async def referral_command_handler(event: Union[types.Message,
|
||||
types.CallbackQuery],
|
||||
async def referral_command_handler(event: Union[types.Message, types.CallbackQuery],
|
||||
settings: Settings, i18n_data: dict,
|
||||
referral_service: ReferralService, bot: Bot,
|
||||
session: AsyncSession):
|
||||
session: AsyncSession,
|
||||
back_callback: str = "main_action:back_to_main"):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
@@ -97,27 +96,35 @@ async def referral_command_handler(event: Union[types.Message,
|
||||
bonus_details_str = "\n".join(bonus_info_parts) if bonus_info_parts else _(
|
||||
"referral_no_bonuses_configured")
|
||||
|
||||
# Get referral statistics
|
||||
referral_stats = await referral_service.get_referral_stats(session, inviter_user_id)
|
||||
|
||||
webapp_referral_link = await _generate_webapp_referral_link(
|
||||
session,
|
||||
settings,
|
||||
inviter_user_id,
|
||||
)
|
||||
webapp_link_section = (
|
||||
_(
|
||||
"referral_webapp_link_line",
|
||||
webapp_referral_link=webapp_referral_link,
|
||||
)
|
||||
if webapp_referral_link
|
||||
else ""
|
||||
)
|
||||
|
||||
text = _("referral_program_info_new",
|
||||
referral_link=referral_link,
|
||||
webapp_link_section=webapp_link_section,
|
||||
bonus_details=bonus_details_str,
|
||||
invited_count=referral_stats["invited_count"],
|
||||
purchased_count=referral_stats["purchased_count"])
|
||||
if settings.SUBSCRIPTION_MINI_APP_URL:
|
||||
db_user = await user_dal.get_user_by_id(session, inviter_user_id)
|
||||
referral_code = await user_dal.ensure_referral_code(session, db_user) if db_user else None
|
||||
webapp_referral_link = _build_webapp_referral_link(
|
||||
settings.SUBSCRIPTION_MINI_APP_URL,
|
||||
referral_code,
|
||||
)
|
||||
if webapp_referral_link:
|
||||
webapp_label = "Web App ссылка" if current_lang == "ru" else "Web App link"
|
||||
text += f"\n\n🔗 {webapp_label}:\n<code>{webapp_referral_link}</code>"
|
||||
|
||||
from bot.keyboards.inline.user_keyboards import get_referral_link_keyboard
|
||||
reply_markup_val = get_referral_link_keyboard(current_lang, i18n)
|
||||
reply_markup_val = get_referral_link_keyboard(
|
||||
current_lang,
|
||||
i18n,
|
||||
back_callback=back_callback,
|
||||
)
|
||||
|
||||
if isinstance(event, types.Message):
|
||||
await event.answer(text,
|
||||
@@ -152,7 +159,7 @@ async def referral_action_handler(callback: types.CallbackQuery, settings: Setti
|
||||
bot_info = await bot.get_me()
|
||||
bot_username = bot_info.username
|
||||
if not bot_username:
|
||||
await callback.answer("Ошибка получения имени бота", show_alert=True)
|
||||
await callback.answer(_("error_generating_referral_link"), show_alert=True)
|
||||
return
|
||||
|
||||
inviter_user_id = callback.from_user.id
|
||||
@@ -166,18 +173,30 @@ async def referral_action_handler(callback: types.CallbackQuery, settings: Setti
|
||||
)
|
||||
await callback.answer(_("error_generating_referral_link"), show_alert=True)
|
||||
return
|
||||
|
||||
friend_message = _("referral_friend_message", referral_link=referral_link)
|
||||
|
||||
|
||||
webapp_referral_link = await _generate_webapp_referral_link(
|
||||
session,
|
||||
settings,
|
||||
inviter_user_id,
|
||||
)
|
||||
if webapp_referral_link:
|
||||
friend_message = _(
|
||||
"referral_friend_message_with_webapp",
|
||||
referral_link=referral_link,
|
||||
webapp_referral_link=webapp_referral_link,
|
||||
)
|
||||
else:
|
||||
friend_message = _("referral_friend_message", referral_link=referral_link)
|
||||
|
||||
await callback.message.answer(
|
||||
friend_message,
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in referral share message: {e}")
|
||||
await callback.answer("Произошла ошибка", show_alert=True)
|
||||
|
||||
await callback.answer(_("error_occurred_try_again"), show_alert=True)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -196,3 +215,25 @@ def _build_webapp_referral_link(base_url: Optional[str], referral_code: Optional
|
||||
parts.fragment,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def _generate_webapp_referral_link(
|
||||
session: AsyncSession,
|
||||
settings: Settings,
|
||||
inviter_user_id: int,
|
||||
) -> Optional[str]:
|
||||
if not settings.SUBSCRIPTION_MINI_APP_URL:
|
||||
return None
|
||||
db_user = await user_dal.get_user_by_id(session, inviter_user_id)
|
||||
referral_code = await user_dal.ensure_referral_code(session, db_user) if db_user else None
|
||||
return _build_webapp_referral_link(
|
||||
settings.SUBSCRIPTION_MINI_APP_URL,
|
||||
referral_code,
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("referral"))
|
||||
async def referral_command_message_handler(message: types.Message, settings: Settings,
|
||||
i18n_data: dict, referral_service: ReferralService,
|
||||
bot: Bot, session: AsyncSession):
|
||||
await referral_command_handler(message, settings, i18n_data, referral_service, bot, session)
|
||||
|
||||
@@ -152,6 +152,11 @@ async def send_bot_interface_menu(
|
||||
settings, subscription_service, session, user_id)
|
||||
|
||||
text = i18n.gettext(current_lang, "bot_interface_menu_title")
|
||||
if settings.SUBSCRIPTION_MINI_APP_URL:
|
||||
text = (
|
||||
f"{text}\n\n"
|
||||
f"{i18n.gettext(current_lang, 'bot_interface_menu_webapp_hint')}"
|
||||
)
|
||||
reply_markup = get_bot_interface_inline_keyboard(
|
||||
current_lang, i18n, settings, show_trial_button_in_menu)
|
||||
|
||||
@@ -797,6 +802,7 @@ async def language_command_handler(
|
||||
event: Union[types.Message, types.CallbackQuery],
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
back_callback: str = "main_action:back_to_main",
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
@@ -804,7 +810,11 @@ async def language_command_handler(
|
||||
) if i18n else key
|
||||
|
||||
text_to_send = _(key="choose_language")
|
||||
reply_markup = get_language_selection_keyboard(i18n, current_lang)
|
||||
reply_markup = get_language_selection_keyboard(
|
||||
i18n,
|
||||
current_lang,
|
||||
back_callback=back_callback,
|
||||
)
|
||||
|
||||
target_message_obj = event.message if isinstance(
|
||||
event, types.CallbackQuery) else event
|
||||
@@ -845,7 +855,8 @@ async def select_language_callback_handler(
|
||||
return
|
||||
|
||||
try:
|
||||
lang_code = callback.data.split("_")[2]
|
||||
lang_payload = callback.data.split("_", 2)[2]
|
||||
lang_code, _, return_target = lang_payload.partition(":")
|
||||
except IndexError:
|
||||
await safe_answer_callback(
|
||||
callback,
|
||||
@@ -879,12 +890,20 @@ async def select_language_callback_handler(
|
||||
exc_info=True)
|
||||
await safe_answer_callback(callback, "Error setting language.", show_alert=True)
|
||||
return
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
if return_target == "bot":
|
||||
await send_bot_interface_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
else:
|
||||
await send_main_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("main_action:"))
|
||||
@@ -896,6 +915,9 @@ async def main_action_callback_handler(
|
||||
action = callback.data.split(":")[1]
|
||||
user_id = callback.from_user.id
|
||||
|
||||
if action in {"back_to_main", "back_to_main_keep", "bot_interface"}:
|
||||
await state.clear()
|
||||
|
||||
from . import subscription as user_subscription_handlers
|
||||
from . import referral as user_referral_handlers
|
||||
from . import promo_user as user_promo_handlers
|
||||
@@ -912,10 +934,29 @@ async def main_action_callback_handler(
|
||||
if action == "subscribe":
|
||||
await user_subscription_handlers.display_subscription_options(
|
||||
callback, i18n_data, settings, session)
|
||||
elif action == "bot_subscribe":
|
||||
await user_subscription_handlers.display_subscription_options(
|
||||
callback,
|
||||
i18n_data,
|
||||
settings,
|
||||
session,
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
elif action == "my_subscription":
|
||||
await user_subscription_handlers.my_subscription_command_handler(
|
||||
callback, i18n_data, settings, panel_service, subscription_service,
|
||||
session, bot)
|
||||
elif action == "bot_my_subscription":
|
||||
await user_subscription_handlers.my_subscription_command_handler(
|
||||
callback,
|
||||
i18n_data,
|
||||
settings,
|
||||
panel_service,
|
||||
subscription_service,
|
||||
session,
|
||||
bot,
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
elif action == "my_devices":
|
||||
await user_subscription_handlers.my_devices_command_handler(
|
||||
callback, i18n_data, settings, panel_service, subscription_service,
|
||||
@@ -923,16 +964,49 @@ async def main_action_callback_handler(
|
||||
elif action == "referral":
|
||||
await user_referral_handlers.referral_command_handler(
|
||||
callback, settings, i18n_data, referral_service, bot, session)
|
||||
elif action == "bot_referral":
|
||||
await user_referral_handlers.referral_command_handler(
|
||||
callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
referral_service,
|
||||
bot,
|
||||
session,
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
elif action == "apply_promo":
|
||||
await user_promo_handlers.prompt_promo_code_input(
|
||||
callback, state, i18n_data, settings, session)
|
||||
elif action == "bot_apply_promo":
|
||||
await user_promo_handlers.prompt_promo_code_input(
|
||||
callback,
|
||||
state,
|
||||
i18n_data,
|
||||
settings,
|
||||
session,
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
elif action == "request_trial":
|
||||
await user_trial_handlers.request_trial_confirmation_handler(
|
||||
callback, settings, i18n_data, subscription_service, session)
|
||||
elif action == "language":
|
||||
|
||||
await language_command_handler(callback, i18n_data, settings)
|
||||
elif action == "info":
|
||||
elif action == "bot_language":
|
||||
await language_command_handler(
|
||||
callback,
|
||||
i18n_data,
|
||||
settings,
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
elif action == "bot_interface":
|
||||
await send_bot_interface_menu(callback,
|
||||
settings,
|
||||
i18n_data,
|
||||
subscription_service,
|
||||
session,
|
||||
is_edit=True)
|
||||
elif action in {"info", "bot_info"}:
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
current_lang = i18n_data.get("current_language",
|
||||
settings.DEFAULT_LANGUAGE)
|
||||
@@ -962,6 +1036,11 @@ async def main_action_callback_handler(
|
||||
i18n,
|
||||
privacy_url,
|
||||
user_agreement_url,
|
||||
back_callback=(
|
||||
"main_action:bot_interface"
|
||||
if callback.data == "main_action:bot_info"
|
||||
else "main_action:back_to_main"
|
||||
),
|
||||
)
|
||||
try:
|
||||
await callback.message.edit_text(_(key="info_links_message"),
|
||||
|
||||
@@ -43,7 +43,13 @@ def _hwid_callback_token(hwid: Optional[str]) -> str:
|
||||
return hashlib.sha256(hwid_str.encode()).hexdigest()[:32]
|
||||
|
||||
|
||||
async def display_subscription_options(event: Union[types.Message, types.CallbackQuery], i18n_data: dict, settings: Settings, session: AsyncSession):
|
||||
async def display_subscription_options(
|
||||
event: Union[types.Message, types.CallbackQuery],
|
||||
i18n_data: dict,
|
||||
settings: Settings,
|
||||
session: AsyncSession,
|
||||
back_callback: str = "main_action:back_to_main",
|
||||
):
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||
|
||||
@@ -94,11 +100,20 @@ async def display_subscription_options(event: Union[types.Message, types.Callbac
|
||||
if options:
|
||||
text_content = get_text("select_traffic_package") if traffic_mode else get_text("select_subscription_period")
|
||||
reply_markup = get_subscription_options_keyboard(
|
||||
options, currency_symbol_val, current_lang, i18n, traffic_mode=traffic_mode
|
||||
options,
|
||||
currency_symbol_val,
|
||||
current_lang,
|
||||
i18n,
|
||||
traffic_mode=traffic_mode,
|
||||
back_callback=back_callback,
|
||||
)
|
||||
else:
|
||||
text_content = get_text("no_subscription_options_available")
|
||||
reply_markup = get_back_to_main_menu_markup(current_lang, i18n)
|
||||
reply_markup = get_back_to_main_menu_markup(
|
||||
current_lang,
|
||||
i18n,
|
||||
callback_data=back_callback,
|
||||
)
|
||||
|
||||
target_message_obj = event.message if isinstance(event, types.CallbackQuery) else event
|
||||
if not target_message_obj:
|
||||
@@ -381,6 +396,7 @@ async def my_subscription_command_handler(
|
||||
subscription_service: SubscriptionService,
|
||||
session: AsyncSession,
|
||||
bot: Bot,
|
||||
back_callback: str = "main_action:back_to_main",
|
||||
):
|
||||
target = event.message if isinstance(event, types.CallbackQuery) else event
|
||||
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||
@@ -404,7 +420,11 @@ async def my_subscription_command_handler(
|
||||
buy_button = InlineKeyboardButton(
|
||||
text=get_text("menu_subscribe_inline"), callback_data="main_action:subscribe"
|
||||
)
|
||||
back_markup = get_back_to_main_menu_markup(current_lang, i18n)
|
||||
back_markup = get_back_to_main_menu_markup(
|
||||
current_lang,
|
||||
i18n,
|
||||
callback_data=back_callback,
|
||||
)
|
||||
|
||||
kb = InlineKeyboardMarkup(inline_keyboard=[[buy_button], *back_markup.inline_keyboard])
|
||||
|
||||
@@ -503,7 +523,11 @@ async def my_subscription_command_handler(
|
||||
if tariff_prefix:
|
||||
text = tariff_prefix + "\n" + text
|
||||
|
||||
base_markup = get_back_to_main_menu_markup(current_lang, i18n)
|
||||
base_markup = get_back_to_main_menu_markup(
|
||||
current_lang,
|
||||
i18n,
|
||||
callback_data=back_callback,
|
||||
)
|
||||
kb = base_markup.inline_keyboard
|
||||
try:
|
||||
local_sub = await subscription_dal.get_active_subscription_by_user_id(session, event.from_user.id)
|
||||
|
||||
@@ -28,11 +28,21 @@ def get_main_menu_inline_keyboard(
|
||||
)
|
||||
)
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="menu_bot_interface_button"),
|
||||
callback_data="main_action:bot_interface"))
|
||||
|
||||
if settings.SUPPORT_LINK:
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="menu_support_button"),
|
||||
url=settings.SUPPORT_LINK))
|
||||
|
||||
user_agreement_url = settings.USER_AGREEMENT_URL or settings.TERMS_OF_SERVICE_URL
|
||||
if settings.PRIVACY_POLICY_URL or user_agreement_url:
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="menu_info_button"),
|
||||
callback_data="main_action:info"))
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
@@ -59,26 +69,26 @@ def get_bot_interface_inline_keyboard(
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="menu_subscribe_inline"),
|
||||
callback_data="main_action:subscribe"))
|
||||
callback_data="main_action:bot_subscribe"))
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=_(key="menu_my_subscription_inline"),
|
||||
callback_data="main_action:my_subscription",
|
||||
callback_data="main_action:bot_my_subscription",
|
||||
)
|
||||
)
|
||||
|
||||
referral_button = InlineKeyboardButton(
|
||||
text=_(key="menu_referral_inline"),
|
||||
callback_data="main_action:referral")
|
||||
callback_data="main_action:bot_referral")
|
||||
promo_button = InlineKeyboardButton(
|
||||
text=_(key="menu_apply_promo_button"),
|
||||
callback_data="main_action:apply_promo")
|
||||
callback_data="main_action:bot_apply_promo")
|
||||
builder.row(referral_button)
|
||||
builder.row(promo_button)
|
||||
|
||||
language_button = InlineKeyboardButton(
|
||||
text=_(key="menu_language_settings_inline"),
|
||||
callback_data="main_action:language")
|
||||
callback_data="main_action:bot_language")
|
||||
status_button_list = []
|
||||
if settings.SERVER_STATUS_URL:
|
||||
status_button_list.append(
|
||||
@@ -99,7 +109,11 @@ def get_bot_interface_inline_keyboard(
|
||||
if settings.PRIVACY_POLICY_URL or user_agreement_url:
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="menu_info_button"),
|
||||
callback_data="main_action:info"))
|
||||
callback_data="main_action:bot_info"))
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="back_to_main_menu_button"),
|
||||
callback_data="main_action:back_to_main"))
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -108,7 +122,8 @@ def get_information_links_keyboard(
|
||||
lang: str,
|
||||
i18n_instance,
|
||||
privacy_policy_url: Optional[str],
|
||||
user_agreement_url: Optional[str]) -> InlineKeyboardMarkup:
|
||||
user_agreement_url: Optional[str],
|
||||
back_callback: str = "main_action:back_to_main") -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
if privacy_policy_url:
|
||||
@@ -121,21 +136,23 @@ def get_information_links_keyboard(
|
||||
url=user_agreement_url))
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="back_to_main_menu_button"),
|
||||
callback_data="main_action:back_to_main"))
|
||||
callback_data=back_callback))
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_language_selection_keyboard(i18n_instance,
|
||||
current_lang: str) -> InlineKeyboardMarkup:
|
||||
current_lang: str,
|
||||
back_callback: str = "main_action:back_to_main") -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(current_lang, key, **kwargs
|
||||
)
|
||||
callback_suffix = ":bot" if back_callback == "main_action:bot_interface" else ""
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=f"🇬🇧 English {'✅' if current_lang == 'en' else ''}",
|
||||
callback_data="set_lang_en")
|
||||
callback_data=f"set_lang_en{callback_suffix}")
|
||||
builder.button(text=f"🇷🇺 Русский {'✅' if current_lang == 'ru' else ''}",
|
||||
callback_data="set_lang_ru")
|
||||
callback_data=f"set_lang_ru{callback_suffix}")
|
||||
builder.button(text=_(key="back_to_main_menu_button"),
|
||||
callback_data="main_action:back_to_main")
|
||||
callback_data=back_callback)
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -154,7 +171,8 @@ def get_trial_confirmation_keyboard(lang: str,
|
||||
|
||||
def get_subscription_options_keyboard(subscription_options: Dict[
|
||||
float, Optional[float]], currency_symbol_val: str, lang: str,
|
||||
i18n_instance, traffic_mode: bool = False) -> InlineKeyboardMarkup:
|
||||
i18n_instance, traffic_mode: bool = False,
|
||||
back_callback: str = "main_action:back_to_main") -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
def _format_gb(val: float) -> str:
|
||||
@@ -181,7 +199,7 @@ def get_subscription_options_keyboard(subscription_options: Dict[
|
||||
builder.adjust(1)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=_(key="back_to_main_menu_button"),
|
||||
callback_data="main_action:back_to_main"))
|
||||
callback_data=back_callback))
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
@@ -413,13 +431,14 @@ def get_yk_saved_cards_keyboard(
|
||||
|
||||
|
||||
def get_referral_link_keyboard(lang: str,
|
||||
i18n_instance) -> InlineKeyboardMarkup:
|
||||
i18n_instance,
|
||||
back_callback: str = "main_action:back_to_main") -> InlineKeyboardMarkup:
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=_(key="referral_share_message_button"),
|
||||
callback_data="referral_action:share_message")
|
||||
builder.button(text=_(key="back_to_main_menu_button"),
|
||||
callback_data="main_action:back_to_main")
|
||||
callback_data=back_callback)
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
+5
-1
@@ -11,6 +11,7 @@
|
||||
"menu_activate_trial_button": "🆓 Free Trial",
|
||||
"menu_subscribe_inline": "🚀 Purchase",
|
||||
"menu_personal_account_button": "🔑 Personal Account",
|
||||
"menu_bot_interface_button": "🤖 Bot menu",
|
||||
"menu_my_subscription_inline": "🔐 My Subscription",
|
||||
"no_subscription_options_available": "Subscription issuance is not configured by the bot administrator",
|
||||
"menu_referral_inline": "🎁 Invite friend",
|
||||
@@ -22,6 +23,7 @@
|
||||
"menu_terms_button": "📄 Terms of Service",
|
||||
"menu_info_button": "ℹ️ Information",
|
||||
"bot_interface_menu_title": "Bot interface",
|
||||
"bot_interface_menu_webapp_hint": "The Personal Account is the easier place to manage the service.",
|
||||
"info_links_message": "Choose a document:",
|
||||
"privacy_policy_button": "🔒 Privacy Policy",
|
||||
"user_agreement_button": "📄 User Agreement",
|
||||
@@ -100,11 +102,13 @@
|
||||
"referral_welcome_bonus_applied": "🎁 You have received {days} bonus day(s) for registering via a referral link!\nYour subscription is active until {end_date}.",
|
||||
"yes_button": "Yes",
|
||||
"no_button": "No",
|
||||
"referral_program_info_new": "🎁 <b>Referral Program</b>\n\n📊 <b>Your stats:</b>\n👥 Friends invited: <b>{invited_count}</b>\n💳 Purchased subscription: <b>{purchased_count}</b>\n\n🔗 Your link:\n<code>{referral_link}</code>\n\n💰 <b>Invitation bonuses:</b>\n{bonus_details}\n\n📢 Share the link with friends and get bonuses!",
|
||||
"referral_program_info_new": "🎁 <b>Referral Program</b>\n\n📊 <b>Your stats:</b>\n👥 Friends invited: <b>{invited_count}</b>\n💳 Purchased subscription: <b>{purchased_count}</b>\n\n🔗 Telegram link:\n<code>{referral_link}</code>{webapp_link_section}\n\n💰 <b>Invitation bonuses:</b>\n{bonus_details}\n\n📢 Share the link with friends and get bonuses!",
|
||||
"referral_bonus_per_period": "\n\n🎁 For a friend's {months}-month subscription:\n ➢ You: <b>{inviter_bonus_days} days</b>\n ➢ Friend: <b>{referee_bonus_days} days</b>",
|
||||
"referral_not_available_for_traffic": "Referral bonuses are not available for traffic packages.",
|
||||
"referral_share_message_button": "📩 Message for friend",
|
||||
"referral_friend_message": "🚀 Hey! Try this service - it's fast, reliable and affordable!\n\n🎁 Use my link to get bonus days with your subscription!\n\n{referral_link}",
|
||||
"referral_friend_message_with_webapp": "🚀 Hey! Try this service - it's fast, reliable and affordable!\n\n🎁 Use my link to get bonus days with your subscription!\n\nTelegram: {referral_link}\nWeb: {webapp_referral_link}",
|
||||
"referral_webapp_link_line": "\n🌐 Web link:\n<code>{webapp_referral_link}</code>",
|
||||
"friend_placeholder": "friend",
|
||||
"referral_bonus_inviter_notification_extended": "🎉 Congrats! Your friend {referee_name} paid for a subscription. You received {days} bonus days! Your subscription is now active until {new_end_date}.",
|
||||
"referral_bonus_inviter_notification_new_sub": "🎉 Congrats! Your friend {referee_name} paid for a subscription. You received a {days}-day bonus subscription! It is active until {new_end_date}.",
|
||||
|
||||
+5
-1
@@ -11,6 +11,7 @@
|
||||
"menu_activate_trial_button": "🆓 Пробный период",
|
||||
"menu_subscribe_inline": "🚀 Купить",
|
||||
"menu_personal_account_button": "🔑 Личный кабинет",
|
||||
"menu_bot_interface_button": "🤖 Бот-меню",
|
||||
"menu_my_subscription_inline": "🔐 Моя подписка",
|
||||
"no_subscription_options_available": "Выдача подписки не настроена администратором бота",
|
||||
"menu_referral_inline": "🎁 Пригласить друга",
|
||||
@@ -22,6 +23,7 @@
|
||||
"menu_terms_button": "📄 Условия сервиса",
|
||||
"menu_info_button": "ℹ️ Информация",
|
||||
"bot_interface_menu_title": "Интерфейс в боте",
|
||||
"bot_interface_menu_webapp_hint": "В личном кабинете взаимодействовать с сервисом будет удобнее.",
|
||||
"info_links_message": "Выберите документ:",
|
||||
"privacy_policy_button": "🔒 Политика конфиденциальности",
|
||||
"user_agreement_button": "📄 Пользовательское соглашение",
|
||||
@@ -100,11 +102,13 @@
|
||||
"referral_welcome_bonus_applied": "🎁 Вам начислено {days} бонусных дн. за регистрацию по реферальной ссылке!\nПодписка активна до {end_date}.",
|
||||
"yes_button": "Да",
|
||||
"no_button": "Нет",
|
||||
"referral_program_info_new": "🎁 <b>Реферальная программа</b>\n\n📊 <b>Твоя статистика:</b>\n👥 Приглашено друзей: <b>{invited_count}</b>\n💳 Купили подписку: <b>{purchased_count}</b>\n\n🔗 Твоя ссылка:\n<code>{referral_link}</code>\n\n💰 <b>Бонусы за приглашения:</b>\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!",
|
||||
"referral_program_info_new": "🎁 <b>Реферальная программа</b>\n\n📊 <b>Твоя статистика:</b>\n👥 Приглашено друзей: <b>{invited_count}</b>\n💳 Купили подписку: <b>{purchased_count}</b>\n\n🔗 Telegram ссылка:\n<code>{referral_link}</code>{webapp_link_section}\n\n💰 <b>Бонусы за приглашения:</b>\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!",
|
||||
"referral_bonus_per_period": "\n\n🎁 За {months}-мес. подписку друга:\n ➢ Вы: <b>{inviter_bonus_days} дн.</b>\n ➢ Друг: <b>{referee_bonus_days} дн.</b>",
|
||||
"referral_not_available_for_traffic": "Для пакетов трафика реферальные бонусы не начисляются.",
|
||||
"referral_share_message_button": "📩 Сообщение для друга",
|
||||
"referral_friend_message": "🚀 Привет! Попробуй этот сервис - быстрый, надёжный и доступный!\n\n🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\n{referral_link}",
|
||||
"referral_friend_message_with_webapp": "🚀 Привет! Попробуй этот сервис - быстрый, надёжный и доступный!\n\n🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\nTelegram: {referral_link}\nWeb: {webapp_referral_link}",
|
||||
"referral_webapp_link_line": "\n🌐 Web ссылка:\n<code>{webapp_referral_link}</code>",
|
||||
"friend_placeholder": "друг",
|
||||
"referral_bonus_inviter_notification_extended": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислено {days} бонусных дней! Ваша подписка теперь активна до {new_end_date}.",
|
||||
"referral_bonus_inviter_notification_new_sub": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислена бонусная подписка на {days} дней! Она активна до {new_end_date}.",
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from bot.app.web import subscription_webapp
|
||||
from bot.handlers.user import referral
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_bot_interface_inline_keyboard,
|
||||
get_information_links_keyboard,
|
||||
get_language_selection_keyboard,
|
||||
get_main_menu_inline_keyboard,
|
||||
get_referral_link_keyboard,
|
||||
get_subscription_options_keyboard,
|
||||
)
|
||||
|
||||
|
||||
class JsonI18nStub:
|
||||
def __init__(self):
|
||||
self.translations = json.loads(
|
||||
Path("locales/en.json").read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
def gettext(self, lang, key, **kwargs):
|
||||
text = self.translations[key]
|
||||
return text.format(**kwargs) if kwargs else text
|
||||
|
||||
|
||||
class UserBotMenuTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.i18n = JsonI18nStub()
|
||||
self.settings = SimpleNamespace(
|
||||
SUBSCRIPTION_MINI_APP_URL="https://app.example.com/",
|
||||
SUPPORT_LINK="https://t.me/support",
|
||||
PRIVACY_POLICY_URL="https://example.com/privacy",
|
||||
USER_AGREEMENT_URL="https://example.com/agreement",
|
||||
TERMS_OF_SERVICE_URL="",
|
||||
TRIAL_ENABLED=True,
|
||||
SERVER_STATUS_URL="",
|
||||
)
|
||||
|
||||
def _callback_data(self, markup):
|
||||
return [
|
||||
button.callback_data
|
||||
for row in markup.inline_keyboard
|
||||
for button in row
|
||||
if button.callback_data
|
||||
]
|
||||
|
||||
def test_main_menu_exposes_bot_menu_and_information(self):
|
||||
markup = get_main_menu_inline_keyboard("en", self.i18n, self.settings)
|
||||
|
||||
callbacks = self._callback_data(markup)
|
||||
|
||||
self.assertIn("main_action:bot_interface", callbacks)
|
||||
self.assertIn("main_action:info", callbacks)
|
||||
|
||||
def test_bot_interface_buttons_return_to_bot_interface(self):
|
||||
markup = get_bot_interface_inline_keyboard("en", self.i18n, self.settings)
|
||||
|
||||
callbacks = self._callback_data(markup)
|
||||
|
||||
self.assertIn("main_action:bot_subscribe", callbacks)
|
||||
self.assertIn("main_action:bot_my_subscription", callbacks)
|
||||
self.assertIn("main_action:bot_referral", callbacks)
|
||||
self.assertIn("main_action:bot_info", callbacks)
|
||||
self.assertIn("main_action:back_to_main", callbacks)
|
||||
|
||||
def test_nested_bot_menu_keyboards_can_target_bot_interface_back(self):
|
||||
subscription_markup = get_subscription_options_keyboard(
|
||||
{1: 100},
|
||||
"RUB",
|
||||
"en",
|
||||
self.i18n,
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
referral_markup = get_referral_link_keyboard(
|
||||
"en",
|
||||
self.i18n,
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
info_markup = get_information_links_keyboard(
|
||||
"en",
|
||||
self.i18n,
|
||||
"https://example.com/privacy",
|
||||
"https://example.com/agreement",
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
language_markup = get_language_selection_keyboard(
|
||||
self.i18n,
|
||||
"en",
|
||||
back_callback="main_action:bot_interface",
|
||||
)
|
||||
|
||||
self.assertIn("main_action:bot_interface", self._callback_data(subscription_markup))
|
||||
self.assertIn("main_action:bot_interface", self._callback_data(referral_markup))
|
||||
self.assertIn("main_action:bot_interface", self._callback_data(info_markup))
|
||||
self.assertIn("set_lang_ru:bot", self._callback_data(language_markup))
|
||||
|
||||
def test_webapp_referral_link_uses_ref_query_and_is_normalized(self):
|
||||
link = referral._build_webapp_referral_link(
|
||||
"https://app.example.com/invite?utm=channel",
|
||||
"AbC123xYz",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
link,
|
||||
"https://app.example.com/invite?utm=channel&ref=uAbC123xYz",
|
||||
)
|
||||
self.assertEqual(subscription_webapp._normalize_referral_param("uAbC123xYz"), "ABC123XYZ")
|
||||
self.assertEqual(subscription_webapp._normalize_referral_param("ref_uAbC123xYz"), "ABC123XYZ")
|
||||
|
||||
def test_referral_text_places_web_link_after_telegram_link(self):
|
||||
text = self.i18n.gettext(
|
||||
"en",
|
||||
"referral_program_info_new",
|
||||
referral_link="https://t.me/bot?start=ref_uABC123XYZ",
|
||||
webapp_link_section=self.i18n.gettext(
|
||||
"en",
|
||||
"referral_webapp_link_line",
|
||||
webapp_referral_link="https://app.example.com/?ref=uABC123XYZ",
|
||||
),
|
||||
bonus_details="bonus",
|
||||
invited_count=1,
|
||||
purchased_count=0,
|
||||
)
|
||||
|
||||
self.assertLess(text.index("Telegram link:"), text.index("Web link:"))
|
||||
self.assertLess(text.index("Web link:"), text.index("Invitation bonuses:"))
|
||||
Reference in New Issue
Block a user