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": "❌ Приложение не найдено",