Compare commits

...

10 Commits

Author SHA1 Message Date
Egor 9311e7c11d Update config.py 2025-08-21 06:11:53 +03:00
Egor 625972da87 Update inline.py 2025-08-21 06:11:08 +03:00
Egor d3293615b0 Update subscription.py 2025-08-21 06:09:28 +03:00
Egor eef1fcc705 Create app-config.json 2025-08-21 06:06:27 +03:00
Egor ffe8259025 Update README.md 2025-08-21 04:48:28 +03:00
Egor bac42b51e1 Update README.md 2025-08-21 02:46:20 +03:00
Egor 510ec2990d Update README.md 2025-08-21 02:40:21 +03:00
Egor dc06056cc7 Update README.md 2025-08-21 02:37:02 +03:00
Egor 103d6e8854 Update .env.example 2025-08-21 02:34:31 +03:00
Egor 466f1e66cc Update README.md 2025-08-21 02:34:15 +03:00
6 changed files with 647 additions and 22 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ AVAILABLE_LANGUAGES=ru
# LOGGING
LOG_LEVEL=INFO
LOG_FILE=logs/bot.log
LOG_FILE=/tmp/bot.log
# DEVELOPMENT
DEBUG=false
+23 -7
View File
@@ -136,7 +136,7 @@ bedolaga_bot/
│ │ ├── referral_service.py # Рефералы
│ │ ├── monitoring_service.py # Мониторинг
│ │ ├── tribute_service.py
│ │ └── remnawave_service.py # Интеграция с RemnaWave
│ │ └── remnawave_service.py # Интеграция с Remnawave
│ │
│ ├── utils/ # Утилиты
│ │ ├── __init__.py
@@ -194,11 +194,11 @@ bedolaga_bot/
| `DATABASE_URL` | URL подключения к БД | `sqlite+aiosqlite:///./bot.db` |
| `REDIS_URL` | URL подключения к Redis | `redis://localhost:6379/0` |
#### 🖥️ RemnaWave API
#### 🖥️ Remnawave API
| Переменная | Описание | Пример |
|------------|----------|---------|
| `REMNAWAVE_API_URL` | URL API RemnaWave | `https://panel.example.com` |
| `REMNAWAVE_API_URL` | URL API Remnawave | `https://panel.example.com` |
| `REMNAWAVE_API_KEY` | JWT токен для API | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...` |
#### 🎁 Тестовая подписка
@@ -280,7 +280,7 @@ bedolaga_bot/
| `DEFAULT_LANGUAGE` | Язык по умолчанию (Англа нет, но все для его реализации есть) | `ru` |
| `AVAILABLE_LANGUAGES` | Доступные языки | `ru,en` |
| `LOG_LEVEL` | Уровень логирования | `INFO` |
| `LOG_FILE` | Файл логов | `logs/bot.log` |
| `LOG_FILE` | Файл логов | `/tmp/bot.log` |
| `DEBUG` | Режим отладки | `false` |
</details>
<details>
@@ -392,6 +392,7 @@ services:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
REDIS_URL: redis://redis:6379/0
volumes:
- ./logs:/app/logs
- ./data:/app/data
@@ -452,8 +453,23 @@ services:
networks:
- bot_network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
networks:
- bot_network
volumes:
postgres_data:
redis_data:
networks:
bot_network:
@@ -478,13 +494,13 @@ networks:
3. Нажмите **Синхронизация**
4. Дождитесь завершения процесса
> ⚠️ **Важно!** Без синхронизации серверов пользователи не смогут выбирать страны при покупке подписки. Сквады из вашей RemnaWave панели автоматически импортируются в бота.
> ⚠️ **Важно!** Без синхронизации серверов пользователи не смогут выбирать страны при покупке подписки. Сквады из вашей Remnawave панели автоматически импортируются в бота.
#### 👥 Синхронизация пользователей (если есть существующая база)
Если у вас уже были пользователи в RemnaWave до установки бота:
Если у вас уже были пользователи в Remnawave до установки бота:
1. Зайдите в **Админ панель****RemnaWave**
1. Зайдите в **Админ панель****Remnawave**
2. Выберите **Синхронизация**
3. Нажмите **Синхронизировать всех**
4. Дождитесь завершения импорта пользователей и их подписок
+163
View File
@@ -0,0 +1,163 @@
{
"ios": [
{
"id": "happ",
"name": "Happ",
"isFeatured": true,
"urlScheme": "happ://add/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://apps.apple.com/us/app/happ-proxy-utility/id6504287215",
"buttonText": {
"en": "App Store [EU]",
"ru": "App Store [EU]"
}
},
{
"buttonLink": "https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973",
"buttonText": {
"en": "App Store [RU]",
"ru": "App Store [RU]"
}
}
],
"description": {
"en": "Open the page in App Store and install the app. Launch it, in the VPN configuration permission window click Allow and enter your passcode.",
"ru": "Откройте страницу в App Store и установите приложение. Вернись на страницу с подпиской"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below — the app will open and the subscription will be added automatically",
"ru": "Нажмите кнопку выше — (Подключить подписку) приложение откроется, и подписка добавится автоматически."
}
},
"connectAndUseStep": {
"description": {
"en": "In the main section, click the large power button in the center to connect to VPN. Don't forget to select a server from the server list. If needed, choose another server from the server list.",
"ru": "В главном разделе нажмите большую кнопку включения в центре для подключения к VPN. Не забудьте выбрать сервер в списке серверов. При необходимости выберите другой сервер из списка серверов."
}
}
}
],
"android": [
{
"id": "happ",
"name": "Happ",
"isFeatured": true,
"urlScheme": "happ://add/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://play.google.com/store/apps/details?id=com.happproxy",
"buttonText": {
"en": "Google Play",
"ru": "Google Play"
}
},
{
"buttonLink": "https://github.com/Happ-proxy/happ-android/releases/latest/download/Happ.apk",
"buttonText": {
"en": "Download APK",
"ru": "Скачать APK"
}
}
],
"description": {
"en": "Open the page in Google Play and install the app. Or install the app directly from the APK file if Google Play is not working.",
"ru": "Откройте страницу в Google Play и установите приложение. Или установите приложение из APK файла напрямую, если Google Play не работает."
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"ru": "Нажмите кнопку выше — (Подключить подписку) приложение откроется, и подписка добавится автоматически."
}
},
"connectAndUseStep": {
"description": {
"en": "Open the app and connect to the server",
"ru": "Откройте приложение и подключитесь к серверу"
}
}
}
],
"pc": [
{
"id": "happ",
"name": "Happ",
"isFeatured": false,
"urlScheme": "happ://add/",
"installationStep": {
"buttons": [
{
"buttonLink": "https://apps.apple.com/us/app/happ-proxy-utility/id6504287215?l=ru",
"buttonText": {
"en": "Mac Os",
"ru": "Mac Os"
}
},
{
"buttonLink": "https://github.com/Happ-proxy/happ-desktop/releases/latest/download/setup-Happ.x86.exe",
"buttonText": {
"en": "Windows",
"ru": "Windows"
}
}
],
"description": {
"en": "Choose the version for your device, click the button below and install the app.",
"ru": "Выберите подходящую версию для вашего устройства, нажмите на кнопку ниже и установите приложение"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"ru": "Нажмите кнопку выше — (Подключить подписку) приложение откроется, и подписка добавится автоматически."
}
},
"connectAndUseStep": {
"description": {
"en": "You can select a server",
"ru": "Выберете локацию, включить VPN"
}
}
}
],
"tv": [
{
"id": "vpn4tv",
"name": "VPN4TV",
"isFeatured": true,
"urlScheme": "",
"installationStep": {
"buttons": [
{
"buttonLink": "https://play.google.com/store/apps/details?id=com.vpn4tv.hiddify",
"buttonText": {
"en": "Google Play",
"ru": "Google Play"
}
}
],
"description": {
"en": "Open the page in Google Play and install the app. Or install the app directly from the APK file if Google Play is not working.",
"ru": "Откройте страницу в Google Play и установите приложение"
}
},
"addSubscriptionStep": {
"description": {
"en": "Click the button below to add subscription",
"ru": "Нажмите кнопку выше — (Скопировать ссылку подписки) ты скопируешь свою подписку, далее на телевизоре открой VPN4TV, следуя инструкция передай telegram боту ссылку, которую ты скопировал"
}
},
"connectAndUseStep": {
"description": {
"en": "Open the app and connect to the server",
"ru": "Приложение автоматически обновится и загрузит нужные конфиги на твой телевизор, подключай VPN"
}
}
}
]
}
+18 -1
View File
@@ -80,6 +80,10 @@ class Settings(BaseSettings):
WEBHOOK_URL: Optional[str] = None
WEBHOOK_PATH: str = "/webhook"
APP_CONFIG_PATH: str = "app-config.json"
ENABLE_DEEP_LINKS: bool = True
APP_CONFIG_CACHE_TTL: int = 3600 # 1 час
@field_validator('LOG_FILE', mode='before')
@classmethod
def ensure_log_dir(cls, v):
@@ -142,6 +146,19 @@ class Settings(BaseSettings):
def is_notifications_enabled(self) -> bool:
return self.ENABLE_NOTIFICATIONS
def get_app_config_path(self) -> str:
if os.path.isabs(self.APP_CONFIG_PATH):
return self.APP_CONFIG_PATH
project_root = Path(__file__).parent.parent
return str(project_root / self.APP_CONFIG_PATH)
def is_deep_links_enabled(self) -> bool:
return self.ENABLE_DEEP_LINKS
def get_app_config_cache_ttl(self) -> int:
return self.APP_CONFIG_CACHE_TTL
model_config = {
"env_file": ".env",
"env_file_encoding": "utf-8"
@@ -168,4 +185,4 @@ TRAFFIC_PRICES = {
100: settings.PRICE_TRAFFIC_100GB,
250: settings.PRICE_TRAFFIC_250GB,
0: settings.PRICE_TRAFFIC_UNLIMITED,
}
}
+299 -9
View File
@@ -1,9 +1,12 @@
import logging
from datetime import datetime
from datetime import datetime, timedelta
from aiogram import Dispatcher, types, F
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
import json
import os
from typing import Dict, List, Any
from app.config import settings, PERIOD_PRICES, TRAFFIC_PRICES
from app.states import SubscriptionStates
@@ -28,7 +31,9 @@ from app.keyboards.inline import (
get_autopay_days_keyboard, get_back_keyboard,
get_extend_subscription_keyboard, get_add_traffic_keyboard,
get_add_devices_keyboard, get_reset_traffic_confirm_keyboard,
get_manage_countries_keyboard
get_manage_countries_keyboard,
get_device_selection_keyboard, get_connection_guide_keyboard,
get_app_selection_keyboard, get_specific_app_keyboard
)
from app.localization.texts import get_texts
from app.services.remnawave_service import RemnaWaveService
@@ -271,11 +276,23 @@ async def activate_trial(
trial_success_text = f"{texts.TRIAL_ACTIVATED}\n\n"
trial_success_text += f"🔗 <b>Ваша ссылка для подключения:</b>\n"
trial_success_text += f"<code>{subscription.subscription_url}</code>\n\n"
trial_success_text += f"📱 Скопируйте эту ссылку и добавьте её в ваш VPN-клиент"
trial_success_text += f"📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве"
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
],
[
InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")
],
[
InlineKeyboardButton(text="⬅️ В главное меню", callback_data="back_to_menu")
]
])
await callback.message.edit_text(
trial_success_text,
reply_markup=get_back_keyboard(db_user.language),
reply_markup=connect_keyboard,
parse_mode="HTML"
)
else:
@@ -1257,11 +1274,23 @@ async def confirm_purchase(
success_text = f"{texts.SUBSCRIPTION_PURCHASED}\n\n"
success_text += f"🔗 <b>Ваша ссылка для подключения:</b>\n"
success_text += f"<code>{subscription.subscription_url}</code>\n\n"
success_text += f"📱 Скопируйте эту ссылку и добавьте её в ваш VPN-клиент"
success_text += f"📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве"
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
],
[
InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")
],
[
InlineKeyboardButton(text="⬅️ В главное меню", callback_data="back_to_menu")
]
])
await callback.message.edit_text(
success_text,
reply_markup=get_back_keyboard(db_user.language),
reply_markup=connect_keyboard,
parse_mode="HTML"
)
else:
@@ -1800,6 +1829,242 @@ async def confirm_reset_devices(
await callback.answer()
async def handle_connect_subscription(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
"""Показать меню выбора устройства для подключения"""
texts = get_texts(db_user.language)
subscription = db_user.subscription
if not subscription or not subscription.subscription_url:
await callback.answer("❌ У вас нет активной подписки или ссылка еще генерируется", show_alert=True)
return
device_text = f"""
📱 <b>Подключить подписку</b>
🔗 <b>Ссылка подписки:</b>
<code>{subscription.subscription_url}</code>
💡 <b>Выберите ваше устройство</b> для получения подробной инструкции по настройке:
"""
await callback.message.edit_text(
device_text,
reply_markup=get_device_selection_keyboard(db_user.language),
parse_mode="HTML"
)
await callback.answer()
async def handle_device_guide(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
device_type = callback.data.split('_')[2]
texts = get_texts(db_user.language)
subscription = db_user.subscription
if not subscription or not subscription.subscription_url:
await callback.answer("❌ Ссылка подписки недоступна", show_alert=True)
return
apps = get_apps_for_device(device_type, db_user.language)
if not apps:
await callback.answer("❌ Приложения для этого устройства не найдены", show_alert=True)
return
featured_app = next((app for app in apps if app.get('isFeatured', False)), apps[0])
guide_text = f"""
📱 <b>Настройка для {get_device_name(device_type, db_user.language)}</b>
🔗 <b>Ссылка подписки:</b>
<code>{subscription.subscription_url}</code>
📋 <b>Рекомендуемое приложение:</b> {featured_app['name']}
<b>Шаг 1 - Установка:</b>
{featured_app['installationStep']['description'][db_user.language]}
<b>Шаг 2 - Добавление подписки:</b>
{featured_app['addSubscriptionStep']['description'][db_user.language]}
<b>Шаг 3 - Подключение:</b>
{featured_app['connectAndUseStep']['description'][db_user.language]}
💡 <b>Как подключить:</b>
1. Установите приложение по ссылке выше
2. Скопируйте ссылку подписки (нажмите на неё)
3. Откройте приложение и вставьте ссылку
4. Подключитесь к серверу
"""
await callback.message.edit_text(
guide_text,
reply_markup=get_connection_guide_keyboard(
subscription.subscription_url,
featured_app,
db_user.language
),
parse_mode="HTML"
)
await callback.answer()
async def handle_app_selection(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
device_type = callback.data.split('_')[2]
texts = get_texts(db_user.language)
subscription = db_user.subscription
apps = get_apps_for_device(device_type, db_user.language)
if not apps:
await callback.answer("❌ Приложения для этого устройства не найдены", show_alert=True)
return
app_text = f"""
📱 <b>Приложения для {get_device_name(device_type, db_user.language)}</b>
Выберите приложение для подключения:
"""
await callback.message.edit_text(
app_text,
reply_markup=get_app_selection_keyboard(device_type, apps, db_user.language),
parse_mode="HTML"
)
await callback.answer()
async def handle_specific_app_guide(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
_, device_type, app_id = callback.data.split('_')
texts = get_texts(db_user.language)
subscription = db_user.subscription
apps = get_apps_for_device(device_type, db_user.language)
app = next((a for a in apps if a['id'] == app_id), None)
if not app:
await callback.answer("❌ Приложение не найдено", show_alert=True)
return
guide_text = f"""
📱 <b>{app['name']} - {get_device_name(device_type, db_user.language)}</b>
🔗 <b>Ссылка подписки:</b>
<code>{subscription.subscription_url}</code>
<b>Шаг 1 - Установка:</b>
{app['installationStep']['description'][db_user.language]}
<b>Шаг 2 - Добавление подписки:</b>
{app['addSubscriptionStep']['description'][db_user.language]}
<b>Шаг 3 - Подключение:</b>
{app['connectAndUseStep']['description'][db_user.language]}
"""
if 'additionalAfterAddSubscriptionStep' in app:
additional = app['additionalAfterAddSubscriptionStep']
guide_text += f"""
<b>{additional['title'][db_user.language]}:</b>
{additional['description'][db_user.language]}
"""
await callback.message.edit_text(
guide_text,
reply_markup=get_specific_app_keyboard(
subscription.subscription_url,
app,
device_type,
db_user.language
),
parse_mode="HTML"
)
await callback.answer()
async def handle_open_subscription_link(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
subscription = db_user.subscription
if not subscription or not subscription.subscription_url:
await callback.answer("❌ Ссылка подписки недоступна", show_alert=True)
return
await callback.answer(url=subscription.subscription_url)
def load_app_config() -> Dict[str, Any]:
try:
from app.config import settings
config_path = settings.get_app_config_path()
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Ошибка загрузки конфига приложений: {e}")
return {}
def get_apps_for_device(device_type: str, language: str = "ru") -> List[Dict[str, Any]]:
config = load_app_config()
device_mapping = {
'ios': 'ios',
'android': 'android',
'windows': 'pc',
'mac': 'pc',
'tv': 'tv'
}
config_key = device_mapping.get(device_type, device_type)
return config.get(config_key, [])
def get_device_name(device_type: str, language: str = "ru") -> str:
if language == "en":
names = {
'ios': 'iPhone/iPad',
'android': 'Android',
'windows': 'Windows',
'mac': 'macOS',
'tv': 'Android TV'
}
else:
names = {
'ios': 'iPhone/iPad',
'android': 'Android',
'windows': 'Windows',
'mac': 'macOS',
'tv': 'Android TV'
}
return names.get(device_type, device_type)
def create_deep_link(app: Dict[str, Any], subscription_url: str) -> str:
from app.config import settings
return subscription_url
def get_reset_devices_confirm_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
@@ -1976,4 +2241,29 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(
apply_countries_changes,
F.data == "countries_apply"
)
)
dp.callback_query.register(
handle_connect_subscription,
F.data == "subscription_connect"
)
dp.callback_query.register(
handle_device_guide,
F.data.startswith("device_guide_")
)
dp.callback_query.register(
handle_app_selection,
F.data.startswith("app_list_")
)
dp.callback_query.register(
handle_specific_app_guide,
F.data.startswith("app_")
)
dp.callback_query.register(
handle_open_subscription_link,
F.data == "open_subscription_link"
)
+143 -4
View File
@@ -101,6 +101,11 @@ def get_subscription_keyboard(
keyboard = []
if has_subscription:
if subscription and subscription.subscription_url:
keyboard.append([
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
])
if is_trial:
keyboard.append([
InlineKeyboardButton(text=texts.MENU_BUY_SUBSCRIPTION, callback_data="subscription_upgrade")
@@ -111,7 +116,7 @@ def get_subscription_keyboard(
row3 = []
row4 = []
row1.append(InlineKeyboardButton(text="🌍 Добавить страны", callback_data="subscription_add_countries"))
row1.append(InlineKeyboardButton(text="🌐 Добавить страны", callback_data="subscription_add_countries"))
if subscription and subscription.traffic_limit_gb > 0:
row1.append(InlineKeyboardButton(text="📈 Добавить трафик", callback_data="subscription_add_traffic"))
@@ -189,7 +194,7 @@ def get_traffic_packages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
(50, texts.TRAFFIC_50GB),
(100, texts.TRAFFIC_100GB),
(250, texts.TRAFFIC_250GB),
(0, texts.TRAFFIC_UNLIMITED) # 0 = безлимит
(0, texts.TRAFFIC_UNLIMITED)
]
for gb, text in packages:
@@ -294,7 +299,6 @@ def get_balance_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
InlineKeyboardButton(text=texts.BALANCE_SUPPORT_REQUEST, callback_data="balance_support")
])
# Назад
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
])
@@ -534,7 +538,6 @@ def get_manage_countries_keyboard(
current_subscription_countries: List[str],
language: str = "ru"
) -> InlineKeyboardMarkup:
"""Клавиатура для управления странами подписки БЕЗ состояний FSM"""
texts = get_texts(language)
keyboard = []
@@ -592,3 +595,139 @@ def get_manage_countries_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_device_selection_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="📱 iOS (iPhone/iPad)", callback_data="device_guide_ios"),
InlineKeyboardButton(text="🤖 Android", callback_data="device_guide_android")
],
[
InlineKeyboardButton(text="💻 Windows", callback_data="device_guide_windows"),
InlineKeyboardButton(text="🍎 macOS", callback_data="device_guide_mac")
],
[
InlineKeyboardButton(text="📺 Android TV", callback_data="device_guide_tv")
],
[
InlineKeyboardButton(text="🔗 Открыть ссылку подписки", callback_data="open_subscription_link")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
])
def get_connection_guide_keyboard(
subscription_url: str,
app: dict,
language: str = "ru"
) -> InlineKeyboardMarkup:
keyboard = []
if 'installationStep' in app and 'buttons' in app['installationStep']:
app_buttons = []
for button in app['installationStep']['buttons']:
button_text = button['buttonText'].get(language, button['buttonText']['en'])
app_buttons.append(
InlineKeyboardButton(text=f"📥 {button_text}", url=button['buttonLink'])
)
if len(app_buttons) == 2:
keyboard.append(app_buttons)
app_buttons = []
if app_buttons:
keyboard.append(app_buttons)
keyboard.append([
InlineKeyboardButton(text="📋 Скопировать ссылку подписки", url=subscription_url)
])
keyboard.extend([
[
InlineKeyboardButton(text="📱 Выбрать другое устройство", callback_data="subscription_connect")
],
[
InlineKeyboardButton(text="⬅️ К подписке", callback_data="menu_subscription")
]
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_app_selection_keyboard(
device_type: str,
apps: list,
language: str = "ru"
) -> InlineKeyboardMarkup:
keyboard = []
for app in apps:
app_name = app['name']
if app.get('isFeatured', False):
app_name = f"{app_name}"
keyboard.append([
InlineKeyboardButton(
text=app_name,
callback_data=f"app_{device_type}_{app['id']}"
)
])
keyboard.extend([
[
InlineKeyboardButton(text="📱 Выбрать другое устройство", callback_data="subscription_connect")
],
[
InlineKeyboardButton(text="⬅️ К подписке", callback_data="menu_subscription")
]
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_specific_app_keyboard(
subscription_url: str,
app: dict,
device_type: str,
language: str = "ru"
) -> InlineKeyboardMarkup:
keyboard = []
if 'installationStep' in app and 'buttons' in app['installationStep']:
app_buttons = []
for button in app['installationStep']['buttons']:
button_text = button['buttonText'].get(language, button['buttonText']['en'])
app_buttons.append(
InlineKeyboardButton(text=f"📥 {button_text}", url=button['buttonLink'])
)
if len(app_buttons) == 2:
keyboard.append(app_buttons)
app_buttons = []
if app_buttons:
keyboard.append(app_buttons)
keyboard.append([
InlineKeyboardButton(text="📋 Скопировать ссылку подписки", url=subscription_url)
])
if 'additionalAfterAddSubscriptionStep' in app and 'buttons' in app['additionalAfterAddSubscriptionStep']:
for button in app['additionalAfterAddSubscriptionStep']['buttons']:
button_text = button['buttonText'].get(language, button['buttonText']['en'])
keyboard.append([
InlineKeyboardButton(text=button_text, url=button['buttonLink'])
])
keyboard.extend([
[
InlineKeyboardButton(text="📋 Другие приложения", callback_data=f"app_list_{device_type}")
],
[
InlineKeyboardButton(text="📱 Выбрать другое устройство", callback_data="subscription_connect")
],
[
InlineKeyboardButton(text="⬅️ К подписке", callback_data="menu_subscription")
]
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)