From ff71f4b637eedee2b4da354e313fa3a659cf6f49 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 25 Sep 2025 08:50:22 +0300 Subject: [PATCH] Add Happ crypto link mode and download flow --- .env.example | 7 + README.md | 7 + app/config.py | 18 ++ app/database/crud/subscription.py | 6 +- app/database/models.py | 1 + app/database/universal_migration.py | 38 ++++ app/external/remnawave_api.py | 4 +- app/handlers/subscription.py | 256 ++++++++++++++++++++++++++- app/keyboards/inline.py | 139 ++++++++++++++- app/services/remnawave_service.py | 14 +- app/services/subscription_service.py | 11 +- locales/en.json | 12 ++ locales/ru.json | 12 ++ 13 files changed, 507 insertions(+), 18 deletions(-) diff --git a/.env.example b/.env.example index 41eb0876..e71c1656 100644 --- a/.env.example +++ b/.env.example @@ -280,11 +280,18 @@ HIDE_SUBSCRIPTION_LINK=false # miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2) # miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3) # link - Открывает ссылку напрямую в браузере (режим 4) +# happ_cryptolink - открывает ссылку Happ из поля cryptoLink (режим 5) CONNECT_BUTTON_MODE=guide # URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom) MINIAPP_CUSTOM_URL= +# Кнопка скачивания приложения Happ (используется в режиме happ_cryptolink) +HAPP_DOWNLOAD_BUTTON_ENABLED=false +HAPP_IOS_APP_URL= +HAPP_ANDROID_APP_URL= +HAPP_DESKTOP_APP_URL= + # Пропустить принятие правил использования бота SKIP_RULES_ACCEPT=false # Пропустить запрос реферального кода diff --git a/README.md b/README.md index 86b0f6dd..f274afcf 100644 --- a/README.md +++ b/README.md @@ -521,11 +521,18 @@ HIDE_SUBSCRIPTION_LINK=false # miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2) # miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3) # link - Открывает ссылку напрямую в браузере (режим 4) +# happ_cryptolink - открывает ссылку Happ из поля cryptoLink (режим 5) CONNECT_BUTTON_MODE=guide # URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom) MINIAPP_CUSTOM_URL= +# Кнопка скачивания приложения Happ (используется в режиме happ_cryptolink) +HAPP_DOWNLOAD_BUTTON_ENABLED=false +HAPP_IOS_APP_URL= +HAPP_ANDROID_APP_URL= +HAPP_DESKTOP_APP_URL= + # Пропустить принятие правил использования бота SKIP_RULES_ACCEPT=false # Пропустить запрос реферального кода diff --git a/app/config.py b/app/config.py index a5bee373..f85cf1e9 100644 --- a/app/config.py +++ b/app/config.py @@ -214,6 +214,10 @@ class Settings(BaseSettings): LOGO_FILE: str = "vpn_logo.png" SKIP_RULES_ACCEPT: bool = False SKIP_REFERRAL_CODE: bool = False + HAPP_DOWNLOAD_BUTTON_ENABLED: bool = False + HAPP_IOS_APP_URL: Optional[str] = None + HAPP_ANDROID_APP_URL: Optional[str] = None + HAPP_DESKTOP_APP_URL: Optional[str] = None DEFAULT_LANGUAGE: str = "ru" AVAILABLE_LANGUAGES: str = "ru,en" @@ -543,6 +547,20 @@ class Settings(BaseSettings): def get_cryptobot_invoice_expires_seconds(self) -> int: return self.CRYPTOBOT_INVOICE_EXPIRES_HOURS * 3600 + def is_happ_download_button_enabled(self) -> bool: + if not self.HAPP_DOWNLOAD_BUTTON_ENABLED: + return False + + links = self.get_happ_download_links() + return any(link for link in links.values()) + + def get_happ_download_links(self) -> Dict[str, Optional[str]]: + return { + "ios": self.HAPP_IOS_APP_URL, + "android": self.HAPP_ANDROID_APP_URL, + "desktop": self.HAPP_DESKTOP_APP_URL, + } + def is_maintenance_mode(self) -> bool: return self.MAINTENANCE_MODE diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index 051c2369..40300c42 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -965,7 +965,8 @@ async def create_subscription( device_limit: int = 1, connected_squads: list = None, remnawave_short_uuid: str = None, - subscription_url: str = "" + subscription_url: str = "", + happ_crypto_link: Optional[str] = None, ) -> Subscription: if end_date is None: @@ -984,7 +985,8 @@ async def create_subscription( device_limit=device_limit, connected_squads=connected_squads, remnawave_short_uuid=remnawave_short_uuid, - subscription_url=subscription_url + subscription_url=subscription_url, + happ_crypto_link=happ_crypto_link, ) db.add(subscription) diff --git a/app/database/models.py b/app/database/models.py index 0a19fe07..6315ea6e 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -435,6 +435,7 @@ class Subscription(Base): traffic_used_gb = Column(Float, default=0.0) subscription_url = Column(String, nullable=True) + happ_crypto_link = Column(String, nullable=True) device_limit = Column(Integer, default=1) diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py index eaa9d068..77d6441d 100644 --- a/app/database/universal_migration.py +++ b/app/database/universal_migration.py @@ -1459,6 +1459,35 @@ async def add_referral_system_columns(): logger.error(f"Ошибка миграции реферальной системы: {e}") return False + +async def add_happ_crypto_link_column(): + logger.info("=== ДОБАВЛЕНИЕ КОЛОНКИ HAPP_CRYPTO_LINK В SUBSCRIPTIONS ===") + + try: + async with engine.begin() as conn: + column_exists = await check_column_exists('subscriptions', 'happ_crypto_link') + + if column_exists: + logger.info("Колонка happ_crypto_link уже существует") + return True + + db_type = await get_database_type() + + if db_type == 'sqlite': + column_def = 'TEXT' + elif db_type == 'mysql': + column_def = 'TEXT' + else: + column_def = 'TEXT' + + await conn.execute(text(f"ALTER TABLE subscriptions ADD COLUMN happ_crypto_link {column_def}")) + logger.info("Колонка happ_crypto_link успешно добавлена") + return True + + except Exception as e: + logger.error(f"Ошибка добавления колонки happ_crypto_link: {e}") + return False + async def create_subscription_conversions_table(): table_exists = await check_table_exists('subscription_conversions') if table_exists: @@ -1729,6 +1758,12 @@ async def run_universal_migration(): referral_migration_success = await add_referral_system_columns() if not referral_migration_success: logger.warning("⚠️ Проблемы с миграцией реферальной системы") + + happ_column_added = await add_happ_crypto_link_column() + if happ_column_added: + logger.info("✅ Колонка happ_crypto_link готова") + else: + logger.warning("⚠️ Не удалось добавить колонку happ_crypto_link") logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ CRYPTOBOT ===") cryptobot_created = await create_cryptobot_payments_table() @@ -1955,6 +1990,7 @@ async def check_migration_status(): "promo_groups_period_discounts_column": False, "promo_groups_auto_assign_column": False, "users_auto_promo_group_assigned_column": False, + "happ_crypto_link_column": False, } status["has_made_first_topup_column"] = await check_column_exists('users', 'has_made_first_topup') @@ -1971,6 +2007,7 @@ async def check_migration_status(): status["promo_groups_period_discounts_column"] = await check_column_exists('promo_groups', 'period_discounts') status["promo_groups_auto_assign_column"] = await check_column_exists('promo_groups', 'auto_assign_total_spent_kopeks') status["users_auto_promo_group_assigned_column"] = await check_column_exists('users', 'auto_promo_group_assigned') + status["happ_crypto_link_column"] = await check_column_exists('subscriptions', 'happ_crypto_link') media_fields_exist = ( await check_column_exists('broadcast_history', 'has_media') and @@ -2007,6 +2044,7 @@ async def check_migration_status(): "promo_groups_period_discounts_column": "Колонка period_discounts у промо-групп", "promo_groups_auto_assign_column": "Колонка auto_assign_total_spent_kopeks у промо-групп", "users_auto_promo_group_assigned_column": "Флаг автоназначения промогруппы у пользователей", + "happ_crypto_link_column": "Колонка happ_crypto_link в subscriptions", } for check_key, check_status in status.items(): diff --git a/app/external/remnawave_api.py b/app/external/remnawave_api.py index ec553efb..aa616b9d 100644 --- a/app/external/remnawave_api.py +++ b/app/external/remnawave_api.py @@ -35,7 +35,7 @@ class RemnaWaveUser: username: str status: UserStatus used_traffic_bytes: int - lifetime_used_traffic_bytes: int + lifetime_used_traffic_bytes: int traffic_limit_bytes: int traffic_limit_strategy: TrafficLimitStrategy expire_at: datetime @@ -48,6 +48,7 @@ class RemnaWaveUser: active_internal_squads: List[Dict[str, str]] created_at: datetime updated_at: datetime + happ: Optional[Dict[str, str]] = None sub_last_user_agent: Optional[str] = None sub_last_opened_at: Optional[datetime] = None online_at: Optional[datetime] = None @@ -603,6 +604,7 @@ class RemnaWaveAPI: active_internal_squads=user_data['activeInternalSquads'], created_at=datetime.fromisoformat(user_data['createdAt'].replace('Z', '+00:00')), updated_at=datetime.fromisoformat(user_data['updatedAt'].replace('Z', '+00:00')), + happ=user_data.get('happ'), sub_last_user_agent=user_data.get('subLastUserAgent'), sub_last_opened_at=self._parse_optional_datetime(user_data.get('subLastOpenedAt')), online_at=self._parse_optional_datetime(user_data.get('onlineAt')), diff --git a/app/handlers/subscription.py b/app/handlers/subscription.py index 4c0b14a2..5fe80b9b 100644 --- a/app/handlers/subscription.py +++ b/app/handlers/subscription.py @@ -41,7 +41,9 @@ from app.keyboards.inline import ( get_device_management_help_keyboard, get_payment_methods_keyboard_with_cart, get_subscription_confirm_keyboard_with_cart, - get_insufficient_balance_keyboard_with_cart + get_insufficient_balance_keyboard_with_cart, + get_happ_download_device_keyboard, + get_happ_download_link_keyboard, ) from app.localization.texts import get_texts from app.services.remnawave_service import RemnaWaveService @@ -882,6 +884,40 @@ async def activate_trial( [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), url=subscription.subscription_url)], [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")], ]) + elif connect_mode == "happ_cryptolink": + happ_link = getattr(subscription, "happ_crypto_link", None) + if not happ_link and remnawave_user and getattr(remnawave_user, "happ", None): + happ_link = (remnawave_user.happ or {}).get("cryptoLink") + + rows = [] + if happ_link: + rows.append([ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + url=happ_link, + ) + ]) + else: + rows.append([ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + callback_data="subscription_connect", + ) + ]) + + if settings.is_happ_download_button_enabled(): + rows.append([ + InlineKeyboardButton( + text=texts.t("HAPP_DOWNLOAD_BUTTON", "📥 Скачать Happ"), + callback_data="happ_download_app", + ) + ]) + + rows.append([ + InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu") + ]) + + connect_keyboard = InlineKeyboardMarkup(inline_keyboard=rows) else: connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), callback_data="subscription_connect")], @@ -3328,6 +3364,40 @@ async def confirm_purchase( [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), url=subscription.subscription_url)], [InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")], ]) + elif connect_mode == "happ_cryptolink": + happ_link = getattr(subscription, "happ_crypto_link", None) + if not happ_link and remnawave_user and getattr(remnawave_user, "happ", None): + happ_link = (remnawave_user.happ or {}).get("cryptoLink") + + rows = [] + if happ_link: + rows.append([ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + url=happ_link, + ) + ]) + else: + rows.append([ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + callback_data="subscription_connect", + ) + ]) + + if settings.is_happ_download_button_enabled(): + rows.append([ + InlineKeyboardButton( + text=texts.t("HAPP_DOWNLOAD_BUTTON", "📥 Скачать Happ"), + callback_data="happ_download_app", + ) + ]) + + rows.append([ + InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu") + ]) + + connect_keyboard = InlineKeyboardMarkup(inline_keyboard=rows) else: connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), callback_data="subscription_connect")], @@ -4110,6 +4180,75 @@ async def handle_connect_subscription( parse_mode="HTML" ) + elif connect_mode == "happ_cryptolink": + crypto_link = getattr(subscription, "happ_crypto_link", None) + + if not crypto_link and subscription.remnawave_short_uuid: + subscription_service = SubscriptionService() + info = await subscription_service.get_subscription_info(subscription.remnawave_short_uuid) + updated = False + + if info: + new_crypto_link = (info.get("happ") or {}).get("cryptoLink") + if new_crypto_link and new_crypto_link != subscription.happ_crypto_link: + subscription.happ_crypto_link = new_crypto_link + crypto_link = new_crypto_link + updated = True + + panel_url = info.get("subscription_url") or info.get("subscriptionUrl") + if panel_url and panel_url != subscription.subscription_url: + subscription.subscription_url = panel_url + updated = True + + if updated: + await db.commit() + await db.refresh(subscription) + + if not crypto_link: + crypto_link = getattr(subscription, "happ_crypto_link", None) + + if not crypto_link: + await callback.answer( + texts.t( + "HAPP_CRYPTO_LINK_UNAVAILABLE", + "⚠️ Ссылка Happ пока недоступна. Попробуйте позже.", + ), + show_alert=True, + ) + return + + keyboard_rows = [[ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + url=crypto_link, + ) + ]] + + if settings.is_happ_download_button_enabled(): + keyboard_rows.append([ + InlineKeyboardButton( + text=texts.t("HAPP_DOWNLOAD_BUTTON", "📥 Скачать Happ"), + callback_data="happ_download_app", + ) + ]) + + keyboard_rows.append([ + InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription") + ]) + + keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_rows) + + await callback.message.edit_text( + texts.t( + "HAPP_CRYPTO_CONNECT_MESSAGE", + """🚀 Подключить Happ + +🔗 Нажмите кнопку ниже, чтобы открыть ссылку Happ:""", + ), + reply_markup=keyboard, + parse_mode="HTML", + ) + else: device_text = texts.t( "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE", @@ -4130,6 +4269,84 @@ async def handle_connect_subscription( await callback.answer() +async def show_happ_download_options( + callback: types.CallbackQuery, + db_user: User, + _: AsyncSession, +): + texts = get_texts(db_user.language) + + if not settings.is_happ_download_button_enabled(): + await callback.answer( + texts.t( + "HAPP_DOWNLOAD_NOT_AVAILABLE", + "⚠️ Ссылки для скачивания Happ не настроены.", + ), + show_alert=True, + ) + return + + links = settings.get_happ_download_links() + if not any(links.values()): + await callback.answer( + texts.t( + "HAPP_DOWNLOAD_NOT_AVAILABLE", + "⚠️ Ссылки для скачивания Happ не настроены.", + ), + show_alert=True, + ) + return + + await callback.message.edit_text( + texts.t( + "HAPP_DOWNLOAD_SELECT_DEVICE", + """📥 Скачать Happ + +Выберите устройство, для которого нужно скачать приложение:""", + ), + reply_markup=get_happ_download_device_keyboard(db_user.language), + parse_mode="HTML", + ) + + await callback.answer() + + +async def show_happ_download_link( + callback: types.CallbackQuery, + db_user: User, + _: AsyncSession, +): + platform = callback.data.split("_")[-1] + texts = get_texts(db_user.language) + links = settings.get_happ_download_links() + link = links.get(platform) + + if not link: + await callback.answer( + texts.t( + "HAPP_DOWNLOAD_LINK_MISSING", + "⚠️ Ссылка для выбранной платформы недоступна.", + ), + show_alert=True, + ) + return + + device_name = get_happ_platform_name(platform, db_user.language) + + await callback.message.edit_text( + texts.t( + "HAPP_DOWNLOAD_LINK_MESSAGE", + """📥 Скачать Happ + +Нажмите кнопку ниже, чтобы скачать приложение для {device_name}.""", + ).format(device_name=device_name), + reply_markup=get_happ_download_link_keyboard(platform, db_user.language), + parse_mode="HTML", + ) + + await callback.answer() + + async def claim_discount_offer( callback: types.CallbackQuery, db_user: User, @@ -4482,7 +4699,8 @@ def get_device_name(device_type: str, language: str = "ru") -> str: 'android': 'Android', 'windows': 'Windows', 'mac': 'macOS', - 'tv': 'Android TV' + 'tv': 'Android TV', + 'desktop': 'PC', } else: names = { @@ -4490,12 +4708,30 @@ def get_device_name(device_type: str, language: str = "ru") -> str: 'android': 'Android', 'windows': 'Windows', 'mac': 'macOS', - 'tv': 'Android TV' + 'tv': 'Android TV', + 'desktop': 'ПК', } - + return names.get(device_type, device_type) +def get_happ_platform_name(platform: str, language: str = "ru") -> str: + if language == "en": + names = { + 'ios': 'iPhone/iPad', + 'android': 'Android', + 'desktop': 'PC', + } + else: + names = { + 'ios': 'iPhone/iPad', + 'android': 'Android', + 'desktop': 'ПК', + } + + return names.get(platform, platform) + + def create_deep_link(app: Dict[str, Any], subscription_url: str) -> str: from app.config import settings @@ -5104,7 +5340,17 @@ def register_handlers(dp: Dispatcher): handle_connect_subscription, F.data == "subscription_connect" ) - + + dp.callback_query.register( + show_happ_download_options, + F.data == "happ_download_app" + ) + + dp.callback_query.register( + show_happ_download_link, + F.data.startswith("happ_download_platform_") + ) + dp.callback_query.register( handle_device_guide, F.data.startswith("device_guide_") diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index c367b562..722254d6 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -88,6 +88,7 @@ def get_main_menu_keyboard( if has_active_subscription and subscription_is_active: connect_mode = settings.CONNECT_BUTTON_MODE subscription_url = getattr(subscription, "subscription_url", None) + happ_crypto_link = getattr(subscription, "happ_crypto_link", None) def _fallback_connect_button() -> InlineKeyboardButton: return InlineKeyboardButton( @@ -122,9 +123,34 @@ def get_main_menu_keyboard( ]) else: keyboard.append([_fallback_connect_button()]) + elif connect_mode == "happ_cryptolink": + if happ_crypto_link: + keyboard.append([ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + url=happ_crypto_link + ) + ]) + elif subscription_url: + keyboard.append([ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + url=subscription_url + ) + ]) + else: + keyboard.append([_fallback_connect_button()]) else: keyboard.append([_fallback_connect_button()]) + if settings.CONNECT_BUTTON_MODE == "happ_cryptolink" and settings.is_happ_download_button_enabled(): + keyboard.append([ + InlineKeyboardButton( + text=texts.t("HAPP_DOWNLOAD_BUTTON", "📥 Скачать Happ"), + callback_data="happ_download_app", + ) + ]) + keyboard.append([ InlineKeyboardButton(text=balance_button_text, callback_data="menu_balance"), InlineKeyboardButton(text=texts.MENU_SUBSCRIPTION, callback_data="menu_subscription") @@ -349,6 +375,31 @@ def get_subscription_keyboard( keyboard.append([ InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), url=subscription.subscription_url) ]) + elif connect_mode == "happ_cryptolink": + happ_link = getattr(subscription, "happ_crypto_link", None) + + if happ_link: + keyboard.append([ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + url=happ_link + ) + ]) + else: + keyboard.append([ + InlineKeyboardButton( + text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), + callback_data="subscription_connect" + ) + ]) + + if settings.is_happ_download_button_enabled(): + keyboard.append([ + InlineKeyboardButton( + text=texts.t("HAPP_DOWNLOAD_BUTTON", "📥 Скачать Happ"), + callback_data="happ_download_app" + ) + ]) else: keyboard.append([ InlineKeyboardButton(text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"), callback_data="subscription_connect") @@ -1237,7 +1288,7 @@ def get_manage_countries_keyboard( def get_device_selection_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: from app.config import settings texts = get_texts(language) - + keyboard = [ [ InlineKeyboardButton(text=texts.t("DEVICE_GUIDE_IOS", "📱 iOS (iPhone/iPad)"), callback_data="device_guide_ios"), @@ -1265,7 +1316,7 @@ def get_device_selection_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKey def get_connection_guide_keyboard( - subscription_url: str, + subscription_url: str, app: dict, language: str = DEFAULT_LANGUAGE ) -> InlineKeyboardMarkup: @@ -1304,6 +1355,90 @@ def get_connection_guide_keyboard( return InlineKeyboardMarkup(inline_keyboard=keyboard) +def get_happ_download_device_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: + texts = get_texts(language) + links = settings.get_happ_download_links() + + buttons: List[List[InlineKeyboardButton]] = [] + platform_buttons: List[InlineKeyboardButton] = [] + + if links.get("ios"): + platform_buttons.append( + InlineKeyboardButton( + text=texts.t("HAPP_DOWNLOAD_IOS", "🍏 iOS"), + callback_data="happ_download_platform_ios", + ) + ) + + if links.get("android"): + platform_buttons.append( + InlineKeyboardButton( + text=texts.t("HAPP_DOWNLOAD_ANDROID", "🤖 Android"), + callback_data="happ_download_platform_android", + ) + ) + + if platform_buttons: + if len(platform_buttons) > 1: + buttons.append(platform_buttons[:2]) + else: + buttons.append([platform_buttons[0]]) + + if len(platform_buttons) > 2: + buttons.append(platform_buttons[2:]) + + if links.get("desktop"): + buttons.append([ + InlineKeyboardButton( + text=texts.t("HAPP_DOWNLOAD_DESKTOP", "💻 ПК"), + callback_data="happ_download_platform_desktop", + ) + ]) + + buttons.append([ + InlineKeyboardButton( + text=texts.t("BACK_TO_SUBSCRIPTION", "⬅️ К подписке"), + callback_data="subscription_connect", + ) + ]) + + return InlineKeyboardMarkup(inline_keyboard=buttons) + + +def get_happ_download_link_keyboard( + platform: str, + language: str = DEFAULT_LANGUAGE, +) -> InlineKeyboardMarkup: + texts = get_texts(language) + links = settings.get_happ_download_links() + keyboard: List[List[InlineKeyboardButton]] = [] + + link = links.get(platform) + if link: + keyboard.append([ + InlineKeyboardButton( + text=texts.t("HAPP_DOWNLOAD_OPEN", "📥 Скачать приложение"), + url=link, + ) + ]) + + keyboard.append([ + InlineKeyboardButton( + text=texts.t("HAPP_DOWNLOAD_CHOOSE_DEVICE", "📱 Выбрать устройство"), + callback_data="happ_download_app", + ) + ]) + + keyboard.append([ + InlineKeyboardButton( + text=texts.t("BACK_TO_SUBSCRIPTION", "⬅️ К подписке"), + callback_data="subscription_connect", + ) + ]) + + return InlineKeyboardMarkup(inline_keyboard=keyboard) + + def get_app_selection_keyboard( device_type: str, apps: list, diff --git a/app/services/remnawave_service.py b/app/services/remnawave_service.py index dd6ee50c..d7672aaf 100644 --- a/app/services/remnawave_service.py +++ b/app/services/remnawave_service.py @@ -637,14 +637,15 @@ class RemnaWaveService: subscription_data = { 'user_id': user.id, 'status': status.value, - 'is_trial': False, + 'is_trial': False, 'end_date': expire_at, 'traffic_limit_gb': traffic_limit_gb, 'traffic_used_gb': traffic_used_gb, 'device_limit': panel_user.get('hwidDeviceLimit', 1) or 1, 'connected_squads': squad_uuids, 'remnawave_short_uuid': panel_user.get('shortUuid'), - 'subscription_url': panel_user.get('subscriptionUrl', '') + 'subscription_url': panel_user.get('subscriptionUrl', ''), + 'happ_crypto_link': (panel_user.get('happ') or {}).get('cryptoLink'), } subscription = await create_subscription(db, **subscription_data) @@ -667,7 +668,8 @@ class RemnaWaveService: device_limit=1, connected_squads=[], remnawave_short_uuid=panel_user.get('shortUuid'), - subscription_url=panel_user.get('subscriptionUrl', '') + subscription_url=panel_user.get('subscriptionUrl', ''), + happ_crypto_link=(panel_user.get('happ') or {}).get('cryptoLink'), ) logger.info(f"✅ Создана базовая подписка для пользователя {user.telegram_id}") except Exception as basic_error: @@ -733,7 +735,11 @@ class RemnaWaveService: panel_url = panel_user.get('subscriptionUrl', '') if not subscription.subscription_url or subscription.subscription_url != panel_url: subscription.subscription_url = panel_url - + + happ_crypto_link = (panel_user.get('happ') or {}).get('cryptoLink') + if subscription.happ_crypto_link != happ_crypto_link: + subscription.happ_crypto_link = happ_crypto_link + active_squads = panel_user.get('activeInternalSquads', []) squad_uuids = [] if isinstance(active_squads, list): diff --git a/app/services/subscription_service.py b/app/services/subscription_service.py index 7e25c427..5198c8a3 100644 --- a/app/services/subscription_service.py +++ b/app/services/subscription_service.py @@ -130,9 +130,10 @@ class SubscriptionService: ) subscription.remnawave_short_uuid = updated_user.short_uuid - subscription.subscription_url = updated_user.subscription_url + subscription.subscription_url = updated_user.subscription_url + subscription.happ_crypto_link = (updated_user.happ or {}).get('cryptoLink') user.remnawave_uuid = updated_user.uuid - + await db.commit() logger.info(f"✅ Создан/обновлен RemnaWave пользователь для подписки {subscription.id}") @@ -188,8 +189,9 @@ class SubscriptionService: ), active_internal_squads=subscription.connected_squads ) - + subscription.subscription_url = updated_user.subscription_url + subscription.happ_crypto_link = (updated_user.happ or {}).get('cryptoLink') await db.commit() status_text = "активным" if is_actually_active else "истёкшим" @@ -230,9 +232,10 @@ class SubscriptionService: async with self.api as api: updated_user = await api.revoke_user_subscription(user.remnawave_uuid) - + subscription.remnawave_short_uuid = updated_user.short_uuid subscription.subscription_url = updated_user.subscription_url + subscription.happ_crypto_link = (updated_user.happ or {}).get('cryptoLink') await db.commit() logger.info(f"✅ Обновлена ссылка подписки для пользователя {user.telegram_id}") diff --git a/locales/en.json b/locales/en.json index bb1b7fc8..865c1d66 100644 --- a/locales/en.json +++ b/locales/en.json @@ -392,8 +392,10 @@ "SUBSCRIPTION_CONNECT_MINIAPP_MESSAGE": "📱 Connect subscription\n\n🚀 Tap the button below to open the subscription in the Telegram mini app:", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 Connect subscription\n\n📱 Tap the button below to open the app:", "SUBSCRIPTION_CONNECT_LINK_MESSAGE": "🚀 Connect subscription\n\n🔗 Tap the button below to open the subscription link:", + "HAPP_CRYPTO_CONNECT_MESSAGE": "🚀 Connect Happ\n\n🔗 Tap the button below to open your Happ link:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 Connect subscription\n\n🔗 Subscription link:\n{subscription_url}\n\n💡 Choose your device to get detailed setup instructions:", "SUBSCRIPTION_LINK_UNAVAILABLE": "❌ Subscription link is unavailable", + "HAPP_CRYPTO_LINK_UNAVAILABLE": "⚠️ Happ link is not available yet. Please try again later.", "SUBSCRIPTION_DEVICE_APPS_NOT_FOUND": "❌ No apps found for this device", "SUBSCRIPTION_DEVICE_GUIDE_TITLE": "📱 Setup for {device_name}", "SUBSCRIPTION_DEVICE_LINK_TITLE": "🔗 Subscription link:", @@ -406,6 +408,16 @@ "SUBSCRIPTION_DEVICE_HOW_TO_STEP2": "2. Copy the subscription link (tap on it)", "SUBSCRIPTION_DEVICE_HOW_TO_STEP3": "3. Open the app and paste the link", "SUBSCRIPTION_DEVICE_HOW_TO_STEP4": "4. Connect to a server", + "HAPP_DOWNLOAD_BUTTON": "📥 Download Happ", + "HAPP_DOWNLOAD_NOT_AVAILABLE": "⚠️ Happ download links are not configured.", + "HAPP_DOWNLOAD_SELECT_DEVICE": "📥 Download Happ\n\nChoose your device to download the app:", + "HAPP_DOWNLOAD_IOS": "🍏 iOS", + "HAPP_DOWNLOAD_ANDROID": "🤖 Android", + "HAPP_DOWNLOAD_DESKTOP": "💻 Desktop", + "HAPP_DOWNLOAD_LINK_MISSING": "⚠️ The link for the selected platform is unavailable.", + "HAPP_DOWNLOAD_LINK_MESSAGE": "📥 Download Happ\n\nTap the button below to download the app for {device_name}.", + "HAPP_DOWNLOAD_OPEN": "📥 Open download page", + "HAPP_DOWNLOAD_CHOOSE_DEVICE": "📱 Choose another device", "SUBSCRIPTION_APPS_TITLE": "📱 Apps for {device_name}", "SUBSCRIPTION_APPS_PROMPT": "Choose an app to connect:", "SUBSCRIPTION_APP_NOT_FOUND": "❌ App not found", diff --git a/locales/ru.json b/locales/ru.json index ff9fa404..5ce8a6fb 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -392,8 +392,10 @@ "SUBSCRIPTION_CONNECT_MINIAPP_MESSAGE": "📱 Подключить подписку\n\n🚀 Нажмите кнопку ниже, чтобы открыть подписку в мини-приложении Telegram:", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 Подключить подписку\n\n📱 Нажмите кнопку ниже, чтобы открыть приложение:", "SUBSCRIPTION_CONNECT_LINK_MESSAGE": "🚀 Подключить подписку\n\n🔗 Нажмите кнопку ниже, чтобы открыть ссылку подписки:", + "HAPP_CRYPTO_CONNECT_MESSAGE": "🚀 Подключить Happ\n\n🔗 Нажмите кнопку ниже, чтобы открыть ссылку Happ:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 Подключить подписку\n\n🔗 Ссылка подписки:\n{subscription_url}\n\n💡 Выберите ваше устройство для получения подробной инструкции по настройке:", "SUBSCRIPTION_LINK_UNAVAILABLE": "❌ Ссылка подписки недоступна", + "HAPP_CRYPTO_LINK_UNAVAILABLE": "⚠️ Ссылка Happ пока недоступна. Попробуйте позже.", "SUBSCRIPTION_DEVICE_APPS_NOT_FOUND": "❌ Приложения для этого устройства не найдены", "SUBSCRIPTION_DEVICE_GUIDE_TITLE": "📱 Настройка для {device_name}", "SUBSCRIPTION_DEVICE_LINK_TITLE": "🔗 Ссылка подписки:", @@ -406,6 +408,16 @@ "SUBSCRIPTION_DEVICE_HOW_TO_STEP2": "2. Скопируйте ссылку подписки (нажмите на неё)", "SUBSCRIPTION_DEVICE_HOW_TO_STEP3": "3. Откройте приложение и вставьте ссылку", "SUBSCRIPTION_DEVICE_HOW_TO_STEP4": "4. Подключитесь к серверу", + "HAPP_DOWNLOAD_BUTTON": "📥 Скачать Happ", + "HAPP_DOWNLOAD_NOT_AVAILABLE": "⚠️ Ссылки для скачивания Happ не настроены.", + "HAPP_DOWNLOAD_SELECT_DEVICE": "📥 Скачать Happ\n\nВыберите устройство, для которого нужно скачать приложение:", + "HAPP_DOWNLOAD_IOS": "🍏 iOS", + "HAPP_DOWNLOAD_ANDROID": "🤖 Android", + "HAPP_DOWNLOAD_DESKTOP": "💻 ПК", + "HAPP_DOWNLOAD_LINK_MISSING": "⚠️ Ссылка для выбранной платформы недоступна.", + "HAPP_DOWNLOAD_LINK_MESSAGE": "📥 Скачать Happ\n\nНажмите кнопку ниже, чтобы скачать приложение для {device_name}.", + "HAPP_DOWNLOAD_OPEN": "📥 Скачать приложение", + "HAPP_DOWNLOAD_CHOOSE_DEVICE": "📱 Выбрать устройство", "SUBSCRIPTION_APPS_TITLE": "📱 Приложения для {device_name}", "SUBSCRIPTION_APPS_PROMPT": "Выберите приложение для подключения:", "SUBSCRIPTION_APP_NOT_FOUND": "❌ Приложение не найдено",