Compare commits

..

54 Commits

Author SHA1 Message Date
Egor d551a7267f Update README.md 2025-08-23 06:27:24 +03:00
Egor 3ddf6b35c6 Update .env.example 2025-08-23 06:26:37 +03:00
Egor c0d3c70683 Update inline.py 2025-08-23 06:25:15 +03:00
Egor 7267433d99 Update subscription.py 2025-08-23 06:22:41 +03:00
Egor 09f3c4d27f Update config.py 2025-08-23 06:21:43 +03:00
Egor 3083915bc8 Update remnawave_api.py 2025-08-22 15:18:21 +03:00
Egor 5c72c04d27 Update texts.py 2025-08-22 12:25:42 +03:00
Egor 826149da3d Update balance.py 2025-08-22 12:19:14 +03:00
Egor f090be328f Update menu.py 2025-08-22 11:40:57 +03:00
Egor ecbfcd2b54 Update texts.py 2025-08-22 11:40:27 +03:00
Egor 2d21b1c542 Update balance.py 2025-08-22 11:17:22 +03:00
Egor c63438af5e Update monitoring_service.py 2025-08-22 11:11:52 +03:00
Egor 8e47013ae2 Update texts.py 2025-08-22 11:09:54 +03:00
Egor 92fa0ebcc1 Update inline.py 2025-08-22 11:09:19 +03:00
Egor f1d23616f1 Update balance.py 2025-08-22 11:08:37 +03:00
Egor d554f2d801 Create subscription_notifications.py 2025-08-22 11:07:10 +03:00
Egor 1fe4608604 Update monitoring_service.py 2025-08-22 11:05:41 +03:00
Egor bea534b352 Update README.md 2025-08-22 09:17:46 +03:00
Egor e993c19a34 Update README.md 2025-08-22 09:14:40 +03:00
Egor 2deb5d1bc1 Update README.md 2025-08-22 09:06:46 +03:00
Egor fb1b0bbcd5 Update README.md 2025-08-22 09:02:36 +03:00
Egor 0308dd6c58 Update README.md 2025-08-22 09:02:15 +03:00
Egor 860bf7fc7a Update user_service.py 2025-08-22 08:01:34 +03:00
Egor c24024db18 Update user_service.py 2025-08-22 07:50:18 +03:00
Egor 7d330838e0 Update auth.py 2025-08-22 07:48:45 +03:00
Egor feed3fff0d Update start.py 2025-08-22 07:47:36 +03:00
Egor 4ddaafd052 Update requirements.txt 2025-08-22 07:24:08 +03:00
Egor 140194a0ad Update admin.py 2025-08-22 07:21:56 +03:00
Egor 7c6adae87f Update user_service.py 2025-08-22 07:21:19 +03:00
Egor 03deb91a25 Update auth.py 2025-08-22 07:20:20 +03:00
Egor ea2caa98ae Update users.py 2025-08-22 07:19:40 +03:00
Egor dab69b9229 Update bot.py 2025-08-22 07:18:40 +03:00
Egor 0ffa86ff36 Update subscription.py 2025-08-22 02:30:55 +03:00
Egor 3b08b71472 Update inline.py 2025-08-22 02:30:23 +03:00
Egor 6c7b1353df Update subscription.py 2025-08-22 02:29:51 +03:00
Egor 9e009d2ded Update subscription_service.py 2025-08-22 02:29:11 +03:00
Egor dc6a829f0f Update texts.py 2025-08-22 01:12:26 +03:00
Egor 3a3a4aa9aa Update inline.py 2025-08-22 01:05:00 +03:00
Egor 99cd22d76b Update subscription.py 2025-08-22 01:04:42 +03:00
Egor 0a920bffe6 Update remnawave.py 2025-08-22 00:22:37 +03:00
Egor 55fbc65231 Update remnawave_service.py 2025-08-22 00:22:05 +03:00
Egor f508ac151b Update admin.py 2025-08-22 00:21:36 +03:00
Egor abec12eed1 Update subscription.py 2025-08-21 21:33:40 +03:00
Egor 9b50fbbe05 Update inline.py 2025-08-21 21:33:19 +03:00
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
23 changed files with 4117 additions and 1131 deletions
+9 -1
View File
@@ -43,6 +43,14 @@ REFERRAL_REGISTRATION_REWARD=5000
REFERRED_USER_REWARD=10000
REFERRAL_COMMISSION_PERCENT=25
# Режим работы кнопки "Подключиться"
# guide - открывает гайд подключения (режим 1)
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
# miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3)
CONNECT_BUTTON_MODE=miniapp_subscription
# URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom)
# MINIAPP_CUSTOM_URL=
# AUTO-PAYMENT SETTINGS
AUTOPAY_WARNING_DAYS=3,1
@@ -73,7 +81,7 @@ AVAILABLE_LANGUAGES=ru
# LOGGING
LOG_LEVEL=INFO
LOG_FILE=logs/bot.log
LOG_FILE=/tmp/bot.log
# DEVELOPMENT
DEBUG=false
+1108 -599
View File
File diff suppressed because it is too large Load Diff
+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"
}
}
}
]
}
+8 -2
View File
@@ -40,7 +40,13 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
except Exception as e:
logger.warning(f"⚠️ Кеш не инициализирован: {e}")
bot = Bot(token=settings.BOT_TOKEN, parse_mode="HTML")
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
bot = Bot(
token=settings.BOT_TOKEN,
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
)
try:
redis_client = redis.from_url(settings.REDIS_URL)
@@ -85,4 +91,4 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
logger.info("✅ Бот успешно настроен")
return bot, dp
return bot, dp
+21 -1
View File
@@ -69,6 +69,9 @@ class Settings(BaseSettings):
TRIBUTE_DONATE_LINK: Optional[str] = None
TRIBUTE_WEBHOOK_PATH: str = "/tribute-webhook"
TRIBUTE_WEBHOOK_PORT: int = 8081
CONNECT_BUTTON_MODE: str = "guide"
MINIAPP_CUSTOM_URL: str = ""
DEFAULT_LANGUAGE: str = "ru"
AVAILABLE_LANGUAGES: str = "ru,en"
@@ -80,6 +83,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 +149,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 +188,4 @@ TRAFFIC_PRICES = {
100: settings.PRICE_TRAFFIC_100GB,
250: settings.PRICE_TRAFFIC_250GB,
0: settings.PRICE_TRAFFIC_UNLIMITED,
}
}
+67 -1
View File
@@ -444,6 +444,72 @@ async def get_subscription_servers(
return servers_info
async def remove_subscription_servers(
db: AsyncSession,
subscription_id: int,
server_squad_ids: List[int]
) -> bool:
try:
from app.database.models import SubscriptionServer
from sqlalchemy import delete
await db.execute(
delete(SubscriptionServer)
.where(
SubscriptionServer.subscription_id == subscription_id,
SubscriptionServer.server_squad_id.in_(server_squad_ids)
)
)
await db.commit()
logger.info(f"🗑️ Удалены серверы {server_squad_ids} из подписки {subscription_id}")
return True
except Exception as e:
logger.error(f"Ошибка удаления серверов из подписки: {e}")
await db.rollback()
return False
async def get_subscription_renewal_cost(
db: AsyncSession,
subscription_id: int,
period_days: int
) -> int:
try:
from app.config import PERIOD_PRICES, TRAFFIC_PRICES, settings
base_price = PERIOD_PRICES.get(period_days, 0)
servers_info = await get_subscription_servers(db, subscription_id)
servers_cost = sum(server_info['paid_price_kopeks'] for server_info in servers_info)
subscription = await db.get(Subscription, subscription_id)
if not subscription:
return base_price
traffic_cost = 0
if subscription.traffic_limit_gb > 0:
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
devices_cost = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
total_cost = base_price + servers_cost + traffic_cost + devices_cost
logger.info(f"💰 Расчет продления подписки {subscription_id} на {period_days} дней:")
logger.info(f" 📅 Период: {base_price/100}")
logger.info(f" 🌍 Серверы: {servers_cost/100}")
logger.info(f" 📊 Трафик: {traffic_cost/100}")
logger.info(f" 📱 Устройства: {devices_cost/100}")
logger.info(f" 💎 ИТОГО: {total_cost/100}")
return total_cost
except Exception as e:
logger.error(f"Ошибка расчета стоимости продления: {e}")
from app.config import PERIOD_PRICES
return PERIOD_PRICES.get(period_days, 0)
async def create_subscription(
db: AsyncSession,
user_id: int,
@@ -482,4 +548,4 @@ async def create_subscription(
await db.refresh(subscription)
logger.info(f"✅ Создана подписка для пользователя {user_id}")
return subscription
return subscription
+108 -57
View File
@@ -1,7 +1,9 @@
import asyncio
import json
import ssl
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Union, Any
from urllib.parse import urlparse
import aiohttp
import logging
from dataclasses import dataclass
@@ -85,13 +87,65 @@ class RemnaWaveAPI:
self.api_key = api_key
self.session: Optional[aiohttp.ClientSession] = None
def _detect_connection_type(self) -> str:
parsed = urlparse(self.base_url)
local_hosts = [
'localhost', '127.0.0.1', 'remnawave',
'remnawave-backend', 'app', 'api'
]
if parsed.hostname in local_hosts:
return "local"
if parsed.hostname:
if (parsed.hostname.startswith('192.168.') or
parsed.hostname.startswith('10.') or
parsed.hostname.startswith('172.') or
parsed.hostname.endswith('.local')):
return "local"
return "external"
async def __aenter__(self):
conn_type = self._detect_connection_type()
logger.info(f"🔗 Подключение к RemnaWave: {self.base_url} (тип: {conn_type})")
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
connector_kwargs = {}
if conn_type == "local":
logger.debug("🏠 Использую локальные заголовки proxy")
headers.update({
'X-Forwarded-For': '127.0.0.1',
'X-Forwarded-Proto': 'https',
'X-Forwarded-Host': 'localhost',
'X-Real-IP': '127.0.0.1',
'Host': 'localhost'
})
if self.base_url.startswith('https://'):
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
connector_kwargs['ssl'] = ssl_context
logger.debug("🔓 SSL проверка отключена для локального HTTPS")
elif conn_type == "external":
logger.debug("🌐 Использую внешнее подключение с полной SSL проверкой")
pass
connector = aiohttp.TCPConnector(**connector_kwargs)
self.session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=30),
headers={
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
headers=headers,
connector=connector
)
return self
@@ -375,6 +429,56 @@ class RemnaWaveAPI:
return response['response']
async def get_user_devices(self, user_uuid: str) -> Dict[str, Any]:
try:
response = await self._make_request('GET', f'/api/hwid/devices/{user_uuid}')
return response['response']
except RemnaWaveAPIError as e:
if e.status_code == 404:
return {'total': 0, 'devices': []}
raise
async def reset_user_devices(self, user_uuid: str) -> bool:
try:
devices_info = await self.get_user_devices(user_uuid)
devices = devices_info.get('devices', [])
if not devices:
return True
failed_count = 0
for device in devices:
device_hwid = device.get('hwid')
if device_hwid:
try:
delete_data = {
"userUuid": user_uuid,
"hwid": device_hwid
}
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
except Exception as device_error:
logger.error(f"Ошибка удаления устройства {device_hwid}: {device_error}")
failed_count += 1
return failed_count < len(devices) / 2
except Exception as e:
logger.error(f"Ошибка при сбросе устройств: {e}")
return False
async def remove_device(self, user_uuid: str, device_hwid: str) -> bool:
try:
delete_data = {
"userUuid": user_uuid,
"hwid": device_hwid
}
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
return True
except Exception as e:
logger.error(f"Ошибка удаления устройства {device_hwid}: {e}")
return False
def _parse_user(self, user_data: Dict) -> RemnaWaveUser:
return RemnaWaveUser(
uuid=user_data['uuid'],
@@ -422,7 +526,6 @@ class RemnaWaveAPI:
)
def format_bytes(bytes_value: int) -> str:
if bytes_value == 0:
return "0 B"
@@ -467,55 +570,3 @@ async def test_api_connection(api: RemnaWaveAPI) -> bool:
except Exception as e:
logger.error(f"API connection test failed: {e}")
return False
async def get_user_devices(self, user_uuid: str) -> Dict[str, Any]:
try:
response = await self._make_request('GET', f'/api/hwid/devices/{user_uuid}')
return response['response']
except RemnaWaveAPIError as e:
if e.status_code == 404:
return {'total': 0, 'devices': []}
raise
async def reset_user_devices(self, user_uuid: str) -> bool:
try:
devices_info = await self.get_user_devices(user_uuid)
devices = devices_info.get('devices', [])
if not devices:
return True
failed_count = 0
for device in devices:
device_hwid = device.get('hwid')
if device_hwid:
try:
delete_data = {
"userUuid": user_uuid,
"hwid": device_hwid
}
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
except Exception as device_error:
logger.error(f"Ошибка удаления устройства {device_hwid}: {device_error}")
failed_count += 1
return failed_count < len(devices) / 2
except Exception as e:
logger.error(f"Ошибка при сбросе устройств: {e}")
return False
async def remove_device(self, user_uuid: str, device_hwid: str) -> bool:
try:
delete_data = {
"userUuid": user_uuid,
"hwid": device_hwid
}
await self._make_request('POST', '/api/hwid/devices/delete', data=delete_data)
return True
except Exception as e:
logger.error(f"Ошибка удаления устройства {device_hwid}: {e}")
return False
+312 -22
View File
@@ -1324,19 +1324,219 @@ async def show_sync_options(
Выберите тип синхронизации:
- <b>Синхронизировать всех</b> - полная синхронизация всех пользователей
- <b>Только новых</b> - создание пользователей из панели, которых нет в боте
- <b>Обновить данные</b> - обновление информации о трафике и подписках
🔄 <b>Синхронизировать всех</b>
• Полная синхронизация всех пользователей
• Создание новых пользователей из панели
• Обновление данных существующих
• Удаление неактуальных подписок
• ⏱️ Время выполнения: 2-5 минут
⚠️ Процесс может занять несколько минут
🆕 <b>Только новых</b>
• Создание пользователей из панели, которых нет в боте
• Быстрое добавление при массовой регистрации
• ⏱️ Время выполнения: 30 секунд - 2 минуты
📈 <b>Обновить данные</b>
• Обновление информации о трафике и подписках
• Синхронизация статуса и лимитов
• Обновление подключенных сквадов
• ⏱️ Время выполнения: 1-3 минуты
⚠️ <b>Важно:</b>
• Во время синхронизации не выполняйте другие операции
• При полной синхронизации подписки пользователей, отсутствующих в панели, будут деактивированы
• Рекомендуется делать полную синхронизацию ежедневно
"""
keyboard = [
[types.InlineKeyboardButton(text="🔄 Синхронизировать всех", callback_data="sync_all_users")],
[types.InlineKeyboardButton(text="🆕 Только новых", callback_data="sync_new_users")],
[types.InlineKeyboardButton(text="📈 Обновить данные", callback_data="sync_update_data")],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
]
await callback.message.edit_text(
text,
reply_markup=get_sync_options_keyboard(db_user.language)
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_sync_recommendations(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
await callback.message.edit_text(
"🔍 Анализируем состояние синхронизации...",
reply_markup=None
)
remnawave_service = RemnaWaveService()
recommendations = await remnawave_service.get_sync_recommendations(db)
priority_emoji = {
"low": "🟢",
"medium": "🟡",
"high": "🔴"
}
text = f"""
💡 <b>Рекомендации по синхронизации</b>
{priority_emoji.get(recommendations['priority'], '🟢')} <b>Приоритет:</b> {recommendations['priority'].upper()}
⏱️ <b>Время выполнения:</b> {recommendations['estimated_time']}
<b>Рекомендуемое действие:</b>
"""
if recommendations['sync_type'] == 'all':
text += "🔄 Полная синхронизация"
elif recommendations['sync_type'] == 'update_only':
text += "📈 Обновление данных"
elif recommendations['sync_type'] == 'new_only':
text += "🆕 Синхронизация новых"
else:
text += "✅ Синхронизация не требуется"
text += "\n\n<b>Причины:</b>\n"
for reason in recommendations['reasons']:
text += f"{reason}\n"
keyboard = []
if recommendations['should_sync'] and recommendations['sync_type'] != 'none':
keyboard.append([
types.InlineKeyboardButton(
text=f"✅ Выполнить рекомендацию",
callback_data=f"sync_{recommendations['sync_type']}_users" if recommendations['sync_type'] != 'update_only' else "sync_update_data"
)
])
keyboard.extend([
[types.InlineKeyboardButton(text="🔄 Другие опции", callback_data="admin_rw_sync")],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
])
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def validate_subscriptions(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
await callback.message.edit_text(
"🔍 Выполняется валидация подписок...\n\nПроверяем данные, может занять несколько минут.",
reply_markup=None
)
remnawave_service = RemnaWaveService()
stats = await remnawave_service.validate_and_fix_subscriptions(db)
# Формируем отчет
if stats['errors'] == 0:
status_emoji = ""
status_text = "успешно завершена"
else:
status_emoji = "⚠️"
status_text = "завершена с ошибками"
text = f"""
{status_emoji} <b>Валидация {status_text}</b>
📊 <b>Результаты:</b>
• 🔍 Проверено подписок: {stats['checked']}
• 🔧 Исправлено подписок: {stats['fixed']}
• ⚠️ Найдено проблем: {stats['issues_found']}
• ❌ Ошибок: {stats['errors']}
"""
if stats['fixed'] > 0:
text += "\n✅ <b>Исправленные проблемы:</b>\n"
text += "• Статусы просроченных подписок\n"
text += "• Отсутствующие данные RemnaWave\n"
text += "• Некорректные лимиты трафика\n"
text += "• Настройки устройств\n"
if stats['errors'] > 0:
text += f"\n⚠️ Обнаружены ошибки при обработке.\nПроверьте логи для подробной информации."
keyboard = [
[types.InlineKeyboardButton(text="🔄 Повторить валидацию", callback_data="sync_validate")],
[types.InlineKeyboardButton(text="🔄 Полная синхронизация", callback_data="sync_all_users")],
[types.InlineKeyboardButton(text="⬅️ К синхронизации", callback_data="admin_rw_sync")]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def cleanup_subscriptions(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
await callback.message.edit_text(
"🧹 Выполняется очистка неактуальных подписок...\n\nУдаляем подписки пользователей, отсутствующих в панели.",
reply_markup=None
)
remnawave_service = RemnaWaveService()
stats = await remnawave_service.cleanup_orphaned_subscriptions(db)
# Формируем отчет
if stats['errors'] == 0:
status_emoji = ""
status_text = "успешно завершена"
else:
status_emoji = "⚠️"
status_text = "завершена с ошибками"
text = f"""
{status_emoji} <b>Очистка {status_text}</b>
📊 <b>Результаты:</b>
• 🔍 Проверено подписок: {stats['checked']}
• 🗑️ Деактивировано: {stats['deactivated']}
• ❌ Ошибок: {stats['errors']}
"""
if stats['deactivated'] > 0:
text += f"\n🗑️ <b>Деактивированные подписки:</b>\n"
text += f"Отключены подписки пользователей, которые\n"
text += f"отсутствуют в панели RemnaWave.\n"
else:
text += f"\n✅ Все подписки актуальны!\nНеактуальных подписок не найдено."
if stats['errors'] > 0:
text += f"\n⚠️ Обнаружены ошибки при обработке.\nПроверьте логи для подробной информации."
keyboard = [
[types.InlineKeyboardButton(text="🔄 Повторить очистку", callback_data="sync_cleanup")],
[types.InlineKeyboardButton(text="🔍 Валидация", callback_data="sync_validate")],
[types.InlineKeyboardButton(text="⬅️ К синхронизации", callback_data="admin_rw_sync")]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
@@ -1347,37 +1547,124 @@ async def sync_users(
):
sync_type = callback.data.split('_')[-2] + "_" + callback.data.split('_')[-1]
progress_text = "🔄 Выполняется синхронизация...\n\n"
if sync_type == "all_users":
progress_text += "📋 Тип: Полная синхронизация\n"
progress_text += "• Создание новых пользователей\n"
progress_text += "• Обновление существующих\n"
progress_text += "• Удаление неактуальных подписок\n"
elif sync_type == "new_users":
progress_text += "📋 Тип: Только новые пользователи\n"
progress_text += "• Создание пользователей из панели\n"
elif sync_type == "update_data":
progress_text += "📋 Тип: Обновление данных\n"
progress_text += "• Обновление информации о трафике\n"
progress_text += "• Синхронизация подписок\n"
progress_text += "\n⏳ Пожалуйста, подождите..."
await callback.message.edit_text(
"🔄 Выполняется синхронизация...\n\nПожалуйста, подождите.",
progress_text,
reply_markup=None
)
remnawave_service = RemnaWaveService()
if sync_type in ["all_users", "new_users", "update_data"]:
sync_map = {
"all_users": "all",
"new_users": "new_only",
"update_data": "update_only"
}
stats = await remnawave_service.sync_users_from_panel(db, sync_map[sync_type])
sync_map = {
"all_users": "all",
"new_users": "new_only",
"update_data": "update_only"
}
stats = await remnawave_service.sync_users_from_panel(db, sync_map.get(sync_type, "all"))
total_operations = stats['created'] + stats['updated'] + stats.get('deleted', 0)
success_operations = stats['created'] + stats['updated'] + stats.get('deleted', 0)
if stats['errors'] == 0:
status_emoji = ""
status_text = "успешно завершена"
elif stats['errors'] < total_operations:
status_emoji = "⚠️"
status_text = "завершена с предупреждениями"
else:
stats = {"created": 0, "updated": 0, "errors": 0}
status_emoji = ""
status_text = "завершена с ошибками"
text = f"""
<b>Синхронизация завершена</b>
{status_emoji} <b>Синхронизация {status_text}</b>
📊 <b>Результат:</b>
- Создано: {stats['created']}
- Обновлено: {stats['updated']}
- Ошибок: {stats['errors']}
"""
if sync_type == "all_users":
text += f"• 🆕 Создано: {stats['created']}\n"
text += f"• 🔄 Обновлено: {stats['updated']}\n"
if 'deleted' in stats:
text += f"• 🗑️ Удалено: {stats['deleted']}\n"
text += f"• ❌ Ошибок: {stats['errors']}\n"
elif sync_type == "new_users":
text += f"• 🆕 Создано: {stats['created']}\n"
text += f"• ❌ Ошибок: {stats['errors']}\n"
if stats['created'] == 0 and stats['errors'] == 0:
text += "\n💡 Новых пользователей не найдено"
elif sync_type == "update_data":
text += f"• 🔄 Обновлено: {stats['updated']}\n"
text += f"• ❌ Ошибок: {stats['errors']}\n"
if stats['updated'] == 0 and stats['errors'] == 0:
text += "\n💡 Все данные актуальны"
if stats['errors'] > 0:
text += f"\n⚠️ <b>Внимание:</b>\n"
text += f"Некоторые операции завершились с ошибками.\n"
text += f"Проверьте логи для получения подробной информации."
if sync_type == "all_users" and 'deleted' in stats and stats['deleted'] > 0:
text += f"\n🗑️ <b>Удаленные подписки:</b>\n"
text += f"Деактивированы подписки пользователей,\n"
text += f"которые отсутствуют в панели RemnaWave."
text += f"\n\n💡 <b>Рекомендации:</b>\n"
if sync_type == "all_users":
text += "• Полная синхронизация выполнена\n"
text += "• Рекомендуется запускать раз в день\n"
elif sync_type == "new_users":
text += "• Синхронизация новых пользователей\n"
text += "• Используйте при массовом добавлении\n"
elif sync_type == "update_data":
text += "• Обновление данных о трафике\n"
text += "• Запускайте для актуализации статистики\n"
keyboard = []
if stats['errors'] > 0:
keyboard.append([
types.InlineKeyboardButton(
text="🔄 Повторить синхронизацию",
callback_data=callback.data
)
])
if sync_type != "all_users":
keyboard.append([
types.InlineKeyboardButton(
text="🔄 Полная синхронизация",
callback_data="sync_all_users"
)
])
keyboard.extend([
[
types.InlineKeyboardButton(text="📊 Статистика системы", callback_data="admin_rw_system"),
types.InlineKeyboardButton(text="🌐 Ноды", callback_data="admin_rw_nodes")
],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
])
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
])
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@@ -1435,6 +1722,9 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(restart_all_nodes, F.data == "admin_restart_all_nodes")
dp.callback_query.register(show_sync_options, F.data == "admin_rw_sync")
dp.callback_query.register(sync_users, F.data.startswith("sync_"))
dp.callback_query.register(show_sync_recommendations, F.data == "sync_recommendations")
dp.callback_query.register(validate_subscriptions, F.data == "sync_validate")
dp.callback_query.register(cleanup_subscriptions, F.data == "sync_cleanup")
dp.callback_query.register(show_squads_management, F.data == "admin_rw_squads")
dp.callback_query.register(show_squad_details, F.data.startswith("admin_squad_manage_"))
@@ -1474,4 +1764,4 @@ def register_handlers(dp: Dispatcher):
process_squad_name,
SquadCreateStates.waiting_for_name,
F.text
)
)
+149 -20
View File
@@ -274,7 +274,7 @@ async def show_user_subscription(
text += f"<b>Окончание:</b> {format_datetime(subscription.end_date)}\n"
text += f"<b>Трафик:</b> {subscription.traffic_used_gb:.1f}/{subscription.traffic_limit_gb} ГБ\n"
text += f"<b>Устройства:</b> {subscription.device_limit}\n"
text += f"<b>Подключенных устройств:</b> {len(subscription.connected_devices) if subscription.connected_devices else 0}\n"
text += f"<b>Подключенных устройств:</b> {subscription.device_limit}\n"
if subscription.is_active:
days_left = (subscription.end_date - datetime.utcnow()).days
@@ -521,7 +521,14 @@ async def show_user_management(
user = profile["user"]
subscription = profile["subscription"]
status_text = "✅ Активен" if user.status == UserStatus.ACTIVE.value else "❌ Заблокирован"
if user.status == UserStatus.ACTIVE.value:
status_text = "✅ Активен"
elif user.status == UserStatus.BLOCKED.value:
status_text = "🚫 Заблокирован"
elif user.status == UserStatus.DELETED.value:
status_text = "🗑️ Удален"
else:
status_text = "❓ Неизвестно"
text = f"""
👤 <b>Управление пользователем</b>
@@ -558,7 +565,7 @@ async def show_user_management(
await callback.message.edit_text(
text,
reply_markup=get_user_management_keyboard(user.id, db_user.language)
reply_markup=get_user_management_keyboard(user.id, user.status, db_user.language)
)
await callback.answer()
@@ -746,6 +753,112 @@ async def show_inactive_users(
)
await callback.answer()
@admin_required
@error_handler
async def confirm_user_unblock(
callback: types.CallbackQuery,
db_user: User
):
user_id = int(callback.data.split('_')[-1])
await callback.message.edit_text(
"✅ <b>Разблокировка пользователя</b>\n\n"
"Вы уверены, что хотите разблокировать этого пользователя?\n"
"Пользователь снова получит доступ к боту.",
reply_markup=get_confirmation_keyboard(
f"admin_user_unblock_confirm_{user_id}",
f"admin_user_manage_{user_id}",
db_user.language
)
)
await callback.answer()
@admin_required
@error_handler
async def unblock_user(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
user_id = int(callback.data.split('_')[-1])
user_service = UserService()
success = await user_service.unblock_user(db, user_id, db_user.id)
if success:
await callback.message.edit_text(
"✅ Пользователь разблокирован",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_manage_{user_id}")]
])
)
else:
await callback.message.edit_text(
"❌ Ошибка разблокировки пользователя",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_manage_{user_id}")]
])
)
await callback.answer()
@admin_required
@error_handler
async def show_user_statistics(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
user_id = int(callback.data.split('_')[-1])
user_service = UserService()
profile = await user_service.get_user_profile(db, user_id)
if not profile:
await callback.answer("❌ Пользователь не найден", show_alert=True)
return
user = profile["user"]
subscription = profile["subscription"]
text = f"📊 <b>Статистика пользователя</b>\n\n"
text += f"👤 {user.full_name} (ID: <code>{user.telegram_id}</code>)\n\n"
text += f"<b>Основная информация:</b>\n"
text += f"• Дней с регистрации: {profile['registration_days']}\n"
text += f"• Баланс: {settings.format_price(user.balance_kopeks)}\n"
text += f"• Транзакций: {profile['transactions_count']}\n"
text += f"• Язык: {user.language}\n\n"
text += f"<b>Подписка:</b>\n"
if subscription:
sub_status = "✅ Активна" if subscription.is_active else "❌ Неактивна"
sub_type = " (триал)" if subscription.is_trial else " (платная)"
text += f"• Статус: {sub_status}{sub_type}\n"
text += f"• Трафик: {subscription.traffic_used_gb:.1f}/{subscription.traffic_limit_gb} ГБ\n"
text += f"• Устройства: {subscription.device_limit}\n"
text += f"• Стран: {len(subscription.connected_squads)}\n"
else:
text += f"• Отсутствует\n"
text += f"\n<b>Реферальная программа:</b>\n"
if user.referred_by_id:
text += f"• Пришел по рефералке\n"
else:
text += f"• Прямая регистрация\n"
text += f"• Реферальный код: <code>{user.referral_code}</code>\n"
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="⬅️ К пользователю", callback_data=f"admin_user_manage_{user_id}")]
])
)
await callback.answer()
@admin_required
@error_handler
@@ -787,23 +900,48 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(
show_user_subscription,
F.data.startswith("admin_user_sub_")
F.data.startswith("admin_user_subscription_")
)
dp.callback_query.register(
show_user_transactions,
F.data.startswith("admin_user_trans_")
F.data.startswith("admin_user_transactions_")
)
dp.callback_query.register(
confirm_user_delete,
F.data.startswith("admin_user_delete_")
show_user_statistics,
F.data.startswith("admin_user_statistics_")
)
dp.callback_query.register(
block_user,
F.data.startswith("admin_user_block_confirm_")
)
dp.callback_query.register(
delete_user_account,
F.data.startswith("admin_user_delete_confirm_")
)
dp.callback_query.register(
confirm_user_block,
F.data.startswith("admin_user_block_")
)
dp.callback_query.register(
unblock_user,
F.data.startswith("admin_user_unblock_confirm_")
)
dp.callback_query.register(
confirm_user_unblock,
F.data.startswith("admin_user_unblock_") & ~F.data.contains("confirm")
)
dp.callback_query.register(
confirm_user_delete,
F.data.startswith("admin_user_delete_")
)
dp.callback_query.register(
handle_users_list_pagination_fixed,
@@ -835,15 +973,6 @@ def register_handlers(dp: Dispatcher):
AdminStates.editing_user_balance
)
dp.callback_query.register(
confirm_user_block,
F.data.startswith("admin_user_block_")
)
dp.callback_query.register(
block_user,
F.data.startswith("admin_user_block_confirm_")
)
dp.callback_query.register(
show_inactive_users,
@@ -853,4 +982,4 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(
cleanup_inactive_users,
F.data == "admin_cleanup_inactive"
)
)
+130 -99
View File
@@ -96,7 +96,8 @@ async def show_balance_history(
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
parse_mode="HTML"
)
await callback.answer()
@@ -112,16 +113,43 @@ async def handle_balance_history_pagination(
@error_handler
async def start_balance_topup(
async def show_payment_methods(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
texts = get_texts(db_user.language)
payment_text = """
💳 <b>Способы пополнения баланса</b>
Выберите удобный для вас способ оплаты:
<b>Telegram Stars</b> - быстро и удобно
💎 <b>Банковская карта</b> - через Tribute
🛠 <b>Через поддержку</b> - другие способы
Выберите способ пополнения:
"""
await callback.message.edit_text(
payment_text,
reply_markup=get_payment_methods_keyboard(0, db_user.language),
parse_mode="HTML"
)
await callback.answer()
@error_handler
async def start_stars_payment(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
"""Начать пополнение - только для Telegram Stars"""
texts = get_texts(db_user.language)
if not settings.TELEGRAM_STARS_ENABLED:
await callback.answer("❌ Пополнение временно недоступно", show_alert=True)
await callback.answer("❌ Пополнение через Stars временно недоступно", show_alert=True)
return
await callback.message.edit_text(
@@ -130,96 +158,15 @@ async def start_balance_topup(
)
await state.set_state(BalanceStates.waiting_for_amount)
await state.update_data(payment_method="stars")
await callback.answer()
@error_handler
async def process_topup_amount(
message: types.Message,
db_user: User,
state: FSMContext
):
"""Обработка введенной суммы - только для Telegram Stars"""
texts = get_texts(db_user.language)
try:
amount_rubles = float(message.text.replace(',', '.'))
if amount_rubles < 1:
await message.answer("❌ Минимальная сумма пополнения: 1 ₽")
return
if amount_rubles > 50000:
await message.answer("❌ Максимальная сумма пополнения: 50,000 ₽")
return
amount_kopeks = int(amount_rubles * 100)
await state.update_data(amount_kopeks=amount_kopeks)
payment_text = texts.TOP_UP_METHODS.format(
amount=texts.format_price(amount_kopeks)
)
await message.answer(
payment_text,
reply_markup=get_payment_methods_keyboard(amount_kopeks, db_user.language)
)
except ValueError:
await message.answer(
texts.INVALID_AMOUNT,
reply_markup=get_back_keyboard(db_user.language)
)
@error_handler
async def process_stars_payment(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext
):
texts = get_texts(db_user.language)
if not settings.TELEGRAM_STARS_ENABLED:
await callback.answer("❌ Оплата Stars временно недоступна", show_alert=True)
return
amount_kopeks = int(callback.data.split('_')[-1])
try:
payment_service = PaymentService(callback.bot)
invoice_link = await payment_service.create_stars_invoice(
amount_kopeks=amount_kopeks,
description=f"Пополнение баланса на {texts.format_price(amount_kopeks)}",
payload=f"balance_{db_user.id}_{amount_kopeks}"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="⭐ Оплатить", url=invoice_link)],
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
])
await callback.message.edit_text(
f"⭐ <b>Оплата через Telegram Stars</b>\n\n"
f"Сумма: {texts.format_price(amount_kopeks)}\n\n"
f"Нажмите кнопку ниже для оплаты:",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Ошибка создания Stars invoice: {e}")
await callback.answer("❌ Ошибка создания платежа", show_alert=True)
await callback.answer()
@error_handler
async def process_tribute_quick_payment(
async def start_tribute_payment(
callback: types.CallbackQuery,
db_user: User
):
"""Быстрое пополнение через Tribute - без выбора суммы"""
texts = get_texts(db_user.language)
if not settings.TRIBUTE_ENABLED:
@@ -242,17 +189,19 @@ async def process_tribute_quick_payment(
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="💳 Перейти к оплате", url=payment_url)],
[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")]
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
])
await callback.message.edit_text(
f"💳 <b>Пополнение банковской картой</b>\n\n"
f"• Введите любую сумму от 50 \n"
f"• Введите любую сумму от 100\n"
f"• Безопасная оплата через Tribute\n"
f"• Мгновенное зачисление на баланс\n"
f"• Принимаем карты Visa, MasterCard, МИР\n\n"
f"• 🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!\n\n"
f"Нажмите кнопку для перехода к оплате:",
reply_markup=keyboard
reply_markup=keyboard,
parse_mode="HTML"
)
except Exception as e:
@@ -281,6 +230,11 @@ async def request_support_topup(
Способ оплаты
Время обработки: 1-24 часа
<b>Доступные способы:</b>
Криптовалюта
Переводы между банками
Другие платежные системы
"""
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
@@ -288,16 +242,93 @@ async def request_support_topup(
text="💬 Написать в поддержку",
url=f"https://t.me/{settings.SUPPORT_USERNAME.lstrip('@')}"
)],
[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")]
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
])
await callback.message.edit_text(
support_text,
reply_markup=keyboard
reply_markup=keyboard,
parse_mode="HTML"
)
await callback.answer()
@error_handler
async def process_topup_amount(
message: types.Message,
db_user: User,
state: FSMContext
):
texts = get_texts(db_user.language)
try:
amount_rubles = float(message.text.replace(',', '.'))
if amount_rubles < 1:
await message.answer("❌ Минимальная сумма пополнения: 1 ₽")
return
if amount_rubles > 50000:
await message.answer("❌ Максимальная сумма пополнения: 50,000 ₽")
return
amount_kopeks = int(amount_rubles * 100)
data = await state.get_data()
payment_method = data.get("payment_method", "stars")
if payment_method == "stars":
await process_stars_payment_amount(message, db_user, amount_kopeks, state)
else:
await message.answer("❌ Неизвестный способ оплаты")
except ValueError:
await message.answer(
texts.INVALID_AMOUNT,
reply_markup=get_back_keyboard(db_user.language)
)
@error_handler
async def process_stars_payment_amount(
message: types.Message,
db_user: User,
amount_kopeks: int,
state: FSMContext
):
texts = get_texts(db_user.language)
if not settings.TELEGRAM_STARS_ENABLED:
await message.answer("❌ Оплата Stars временно недоступна")
return
try:
payment_service = PaymentService(message.bot)
invoice_link = await payment_service.create_stars_invoice(
amount_kopeks=amount_kopeks,
description=f"Пополнение баланса на {texts.format_price(amount_kopeks)}",
payload=f"balance_{db_user.id}_{amount_kopeks}"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="⭐ Оплатить", url=invoice_link)],
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
])
await message.answer(
f"⭐ <b>Оплата через Telegram Stars</b>\n\n"
f"Сумма: {texts.format_price(amount_kopeks)}\n\n"
f"Нажмите кнопку ниже для оплаты:",
reply_markup=keyboard,
parse_mode="HTML"
)
await state.clear()
except Exception as e:
logger.error(f"Ошибка создания Stars invoice: {e}")
await message.answer("❌ Ошибка создания платежа")
def register_handlers(dp: Dispatcher):
dp.callback_query.register(
@@ -316,26 +347,26 @@ def register_handlers(dp: Dispatcher):
)
dp.callback_query.register(
start_balance_topup,
show_payment_methods,
F.data == "balance_topup"
)
dp.callback_query.register(
process_stars_payment,
F.data.startswith("pay_stars_")
start_stars_payment,
F.data == "topup_stars"
)
dp.callback_query.register(
process_tribute_quick_payment,
F.data == "tribute_quick_pay"
start_tribute_payment,
F.data == "topup_tribute"
)
dp.callback_query.register(
request_support_topup,
F.data == "balance_support"
F.data == "topup_support"
)
dp.message.register(
process_topup_amount,
BalanceStates.waiting_for_amount
)
)
+15 -5
View File
@@ -108,15 +108,25 @@ async def handle_back_to_menu(
def _get_subscription_status(user: User, texts) -> str:
if not user.subscription:
return texts.SUBSCRIPTION_NONE
return "❌ Отсутствует"
if user.subscription.is_trial:
return f"{texts.SUBSCRIPTION_TRIAL} (до {user.subscription.end_date.strftime('%d.%m.%Y')})"
days_left = user.subscription.days_left
if days_left > 1:
return f"🎁 Тестовая подписка\n📅 до {user.subscription.end_date.strftime('%d.%m.%Y')} ({days_left} дн.)"
else:
return f"🎁 Тестовая подписка\n⚠️ истекает сегодня!"
elif user.subscription.is_active:
days_left = user.subscription.days_left
return f"{texts.SUBSCRIPTION_ACTIVE} ({days_left} дн.)"
if days_left > 7:
return f"✅ Активна\n📅 до {user.subscription.end_date.strftime('%d.%m.%Y')} ({days_left} дн.)"
elif days_left > 0:
return f"✅ Активна\n⚠️ истекает через {days_left} дн."
else:
return f"✅ Активна\n⚠️ истекает сегодня!"
else:
return texts.SUBSCRIPTION_EXPIRED
return f"⏰ Истекла\n📅 {user.subscription.end_date.strftime('%d.%m.%Y')}"
def register_handlers(dp: Dispatcher):
@@ -129,4 +139,4 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(
show_service_rules,
F.data == "menu_rules"
)
)
+162 -52
View File
@@ -9,6 +9,7 @@ from app.states import RegistrationStates
from app.database.crud.user import (
get_user_by_telegram_id, create_user, get_user_by_referral_code
)
from app.database.models import UserStatus
from app.keyboards.inline import (
get_rules_keyboard, get_main_menu_keyboard
)
@@ -19,7 +20,7 @@ from app.utils.user_utils import generate_unique_referral_code
logger = logging.getLogger(__name__)
async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession):
async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession, db_user=None):
logger.info(f"🚀 START: Обработка /start от {message.from_user.id}")
referral_code = None
@@ -31,10 +32,10 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession)
if referral_code:
await state.set_data({'referral_code': referral_code})
user = await get_user_by_telegram_id(db, message.from_user.id)
user = db_user if db_user else await get_user_by_telegram_id(db, message.from_user.id)
if user:
logger.info(f"Пользователь найден: {user.telegram_id}")
if user and user.status != UserStatus.DELETED.value:
logger.info(f"Активный пользователь найден: {user.telegram_id}")
texts = get_texts(user.language)
if referral_code and not user.referred_by_id:
@@ -60,26 +61,82 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession)
subscription_is_active=subscription_is_active
)
)
await state.clear()
return
if user and user.status == UserStatus.DELETED.value:
logger.info(f"🔄 Удаленный пользователь {user.telegram_id} начинает повторную регистрацию")
try:
from app.services.user_service import UserService
from app.database.models import (
Subscription, Transaction, PromoCodeUse,
ReferralEarning, SubscriptionServer
)
from sqlalchemy import delete
if user.subscription:
await db.execute(
delete(SubscriptionServer).where(
SubscriptionServer.subscription_id == user.subscription.id
)
)
logger.info(f"🗑️ Удалены записи SubscriptionServer")
if user.subscription:
await db.delete(user.subscription)
logger.info(f"🗑️ Удалена подписка пользователя")
await db.execute(
delete(PromoCodeUse).where(PromoCodeUse.user_id == user.id)
)
await db.execute(
delete(ReferralEarning).where(ReferralEarning.user_id == user.id)
)
await db.execute(
delete(ReferralEarning).where(ReferralEarning.referral_id == user.id)
)
await db.execute(
delete(Transaction).where(Transaction.user_id == user.id)
)
user.status = UserStatus.ACTIVE.value
user.balance_kopeks = 0
user.remnawave_uuid = None
user.has_had_paid_subscription = False
user.referred_by_id = None
from app.utils.user_utils import generate_unique_referral_code
user.referral_code = await generate_unique_referral_code(db, user.telegram_id)
await db.commit()
logger.info(f"✅ Пользователь {user.telegram_id} подготовлен к восстановлению")
except Exception as e:
logger.error(f"❌ Ошибка подготовки к восстановлению: {e}")
await db.rollback()
else:
logger.info(f"🆕 Новый пользователь, начинаем регистрацию")
language = 'ru'
texts = get_texts(language)
data = await state.get_data() or {}
data['language'] = language
await state.set_data(data)
logger.info(f"💾 Установлен русский язык по умолчанию")
await message.answer(
texts.RULES_TEXT,
reply_markup=get_rules_keyboard(language)
)
logger.info(f"📋 Правила отправлены")
await state.set_state(RegistrationStates.waiting_for_rules_accept)
current_state = await state.get_state()
logger.info(f"📊 Установлено состояние: {current_state}")
language = 'ru'
texts = get_texts(language)
data = await state.get_data() or {}
data['language'] = language
await state.set_data(data)
logger.info(f"💾 Установлен русский язык по умолчанию")
await message.answer(
texts.RULES_TEXT,
reply_markup=get_rules_keyboard(language)
)
logger.info(f"📋 Правила отправлены")
await state.set_state(RegistrationStates.waiting_for_rules_accept)
current_state = await state.get_state()
logger.info(f"📊 Установлено состояние: {current_state}")
async def process_rules_accept(
@@ -125,7 +182,7 @@ async def process_rules_accept(
if referrer:
data['referrer_id'] = referrer.id
await state.set_data(data)
logger.info(f"✅ Референс найден: {referrer.id}")
logger.info(f"✅ Реферер найден: {referrer.id}")
await complete_registration_from_callback(callback, state, db)
else:
@@ -237,8 +294,9 @@ async def complete_registration_from_callback(
logger.info(f"🏁 COMPLETE: Завершение регистрации для пользователя {callback.from_user.id}")
existing_user = await get_user_by_telegram_id(db, callback.from_user.id)
if existing_user:
logger.warning(f"⚠️ Пользователь {callback.from_user.id} уже существует! Показываем главное меню.")
if existing_user and existing_user.status == UserStatus.ACTIVE.value:
logger.warning(f"⚠️ Пользователь {callback.from_user.id} уже активен! Показываем главное меню.")
texts = get_texts(existing_user.language)
has_active_subscription = existing_user.subscription is not None
@@ -282,18 +340,43 @@ async def complete_registration_from_callback(
if referrer:
referrer_id = referrer.id
referral_code = await generate_unique_referral_code(db, callback.from_user.id)
user = await create_user(
db=db,
telegram_id=callback.from_user.id,
username=callback.from_user.username,
first_name=callback.from_user.first_name,
last_name=callback.from_user.last_name,
language=language,
referred_by_id=referrer_id,
referral_code=referral_code
)
if existing_user and existing_user.status == UserStatus.DELETED.value:
logger.info(f"🔄 Восстанавливаем удаленного пользователя {callback.from_user.id}")
existing_user.username = callback.from_user.username
existing_user.first_name = callback.from_user.first_name
existing_user.last_name = callback.from_user.last_name
existing_user.language = language
existing_user.referred_by_id = referrer_id
existing_user.status = UserStatus.ACTIVE.value
existing_user.balance_kopeks = 0
existing_user.has_had_paid_subscription = False
from datetime import datetime
existing_user.updated_at = datetime.utcnow()
existing_user.last_activity = datetime.utcnow()
await db.commit()
await db.refresh(existing_user)
user = existing_user
logger.info(f"✅ Пользователь {callback.from_user.id} восстановлен")
else:
logger.info(f"🆕 Создаем нового пользователя {callback.from_user.id}")
referral_code = await generate_unique_referral_code(db, callback.from_user.id)
user = await create_user(
db=db,
telegram_id=callback.from_user.id,
username=callback.from_user.username,
first_name=callback.from_user.first_name,
last_name=callback.from_user.last_name,
language=language,
referred_by_id=referrer_id,
referral_code=referral_code
)
if referrer_id:
try:
@@ -355,7 +438,7 @@ async def complete_registration_from_callback(
except Exception as final_error:
logger.error(f"❌ Критическая ошибка при отправке простого сообщения: {final_error}")
logger.info(f"Зарегистрирован новый пользователь: {user_telegram_id}")
logger.info(f"Регистрация завершена для пользователя: {user_telegram_id}")
async def complete_registration(
@@ -366,6 +449,8 @@ async def complete_registration(
logger.info(f"🏁 COMPLETE: Завершение регистрации для пользователя {message.from_user.id}")
existing_user = await get_user_by_telegram_id(db, message.from_user.id)
data = await state.get_data()
language = data.get('language', 'ru')
texts = get_texts(language)
@@ -376,18 +461,43 @@ async def complete_registration(
if referrer:
referrer_id = referrer.id
referral_code = await generate_unique_referral_code(db, message.from_user.id)
user = await create_user(
db=db,
telegram_id=message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name,
language=language,
referred_by_id=referrer_id,
referral_code=referral_code
)
if existing_user and existing_user.status == UserStatus.DELETED.value:
logger.info(f"🔄 Восстанавливаем удаленного пользователя {message.from_user.id}")
existing_user.username = message.from_user.username
existing_user.first_name = message.from_user.first_name
existing_user.last_name = message.from_user.last_name
existing_user.language = language
existing_user.referred_by_id = referrer_id
existing_user.status = UserStatus.ACTIVE.value
existing_user.balance_kopeks = 0
existing_user.has_had_paid_subscription = False
from datetime import datetime
existing_user.updated_at = datetime.utcnow()
existing_user.last_activity = datetime.utcnow()
await db.commit()
await db.refresh(existing_user)
user = existing_user
logger.info(f"✅ Пользователь {message.from_user.id} восстановлен")
else:
logger.info(f"🆕 Создаем нового пользователя {message.from_user.id}")
referral_code = await generate_unique_referral_code(db, message.from_user.id)
user = await create_user(
db=db,
telegram_id=message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name,
language=language,
referred_by_id=referrer_id,
referral_code=referral_code
)
if referrer_id:
try:
@@ -449,7 +559,7 @@ async def complete_registration(
except:
pass
logger.info(f"Зарегистрирован новый пользователь: {user_telegram_id}")
logger.info(f"Регистрация завершена для пользователя: {user_telegram_id}")
def _get_subscription_status(user, texts):
@@ -504,4 +614,4 @@ def register_handlers(dp: Dispatcher):
)
logger.info("✅ Зарегистрирован process_referral_code_input")
logger.info("🔧 === КОНЕЦ регистрации обработчиков start.py ===")
logger.info("🔧 === КОНЕЦ регистрации обработчиков start.py ===")
+590 -102
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, Tuple
from app.config import settings, PERIOD_PRICES, TRAFFIC_PRICES
from app.states import SubscriptionStates
@@ -28,7 +31,10 @@ 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,
get_subscription_settings_keyboard, get_extend_subscription_keyboard_with_prices
)
from app.localization.texts import get_texts
from app.services.remnawave_service import RemnaWaveService
@@ -144,67 +150,37 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
if subscription.is_trial:
return 0
from app.database.crud.transaction import get_user_transactions
from app.database.models import TransactionType
from app.config import TRAFFIC_PRICES, PERIOD_PRICES, settings
from app.services.subscription_service import SubscriptionService
transactions = await get_user_transactions(db, subscription.user_id, limit=100)
subscription_service = SubscriptionService()
logger.info(f"🔍 Всего транзакций у пользователя {subscription.user_id}: {len(transactions)}")
try:
servers_cost, _ = await subscription_service.get_countries_price_by_uuids(
subscription.connected_squads, db
)
except AttributeError:
logger.warning("Используем fallback для расчета стоимости серверов")
servers_cost, _ = await get_countries_price_by_uuids_fallback(
subscription.connected_squads, db
)
total_subscription_cost = 0
base_subscription_cost = 0
additions_cost = 0
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
for transaction in transactions:
if transaction.type == TransactionType.SUBSCRIPTION_PAYMENT.value:
description = transaction.description.lower()
amount = transaction.amount_kopeks
logger.info(f"📝 Транзакция: {amount/100}₽ - '{transaction.description}'")
if 'сброс' in description:
logger.info(f" ⏭️ Пропускаем сброс")
continue
is_base_subscription = (
('подписка' in description and 'дней' in description) or
('покупка' in description and 'подписка' in description) or
('subscription' in description and 'days' in description)
)
if is_base_subscription:
base_subscription_cost += amount
logger.info(f" 💎 Основная подписка: +{amount/100}")
continue
is_addition = any(keyword in description for keyword in [
'добавление', 'добавить', 'продление', 'продлить'
])
if is_addition:
additions_cost += amount
logger.info(f" 🔧 Дополнение: +{amount/100}")
continue
if amount >= 50000:
base_subscription_cost += amount
logger.info(f" 💰 Крупная транзакция подписки: +{amount/100}")
continue
logger.info(f" ❓ Неопознанная транзакция, пропускаем")
devices_cost = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
total_subscription_cost = base_subscription_cost + additions_cost
base_cost = min(PERIOD_PRICES.values()) if PERIOD_PRICES else 0
if total_subscription_cost > 0:
logger.info(f"💰 Итого стоимость подписки:")
logger.info(f" 📦 Базовая подписка: {base_subscription_cost/100}")
logger.info(f" 🔧 Дополнения: {additions_cost/100}")
logger.info(f" 💎 ОБЩАЯ СТОИМОСТЬ: {total_subscription_cost/100}")
return total_subscription_cost
else:
logger.warning(f"⚠️ Стоимость подписки не найдена для пользователя {subscription.user_id}")
return 0
total_cost = base_cost + servers_cost + traffic_cost + devices_cost
logger.info(f"📊 Расчет стоимости подписки {subscription.id} (по текущим ценам):")
logger.info(f" 📦 Базовая стоимость: {base_cost/100}")
logger.info(f" 🌍 Серверы ({len(subscription.connected_squads)}) по текущим ценам: {servers_cost/100}")
logger.info(f" 📊 Трафик ({subscription.traffic_limit_gb} ГБ): {traffic_cost/100}")
logger.info(f" 📱 Устройства ({subscription.device_limit}): {devices_cost/100}")
logger.info(f" 💎 ОБЩАЯ СТОИМОСТЬ: {total_cost/100}")
return total_cost
except Exception as e:
logger.error(f"❌ Ошибка расчета стоимости подписки: {e}")
@@ -271,11 +247,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:
@@ -373,6 +361,36 @@ async def handle_add_countries(
await callback.answer()
async def get_countries_price_by_uuids_fallback(country_uuids: List[str], db: AsyncSession) -> Tuple[int, List[int]]:
try:
from app.database.crud.server_squad import get_server_squad_by_uuid
total_price = 0
prices_list = []
for country_uuid in country_uuids:
try:
server = await get_server_squad_by_uuid(db, country_uuid)
if server and server.is_available and not server.is_full:
price = server.price_kopeks
total_price += price
prices_list.append(price)
else:
default_price = 1000
total_price += default_price
prices_list.append(default_price)
except Exception:
default_price = 1000
total_price += default_price
prices_list.append(default_price)
return total_price, prices_list
except Exception as e:
logger.error(f"Ошибка fallback функции: {e}")
default_prices = [1000] * len(country_uuids)
return sum(default_prices), default_prices
async def handle_manage_country(
callback: types.CallbackQuery,
db_user: User,
@@ -429,13 +447,14 @@ async def apply_countries_changes(
logger.info(f"🔍 Применение изменений стран")
data = await state.get_data()
new_countries = data.get('countries', [])
texts = get_texts(db_user.language)
subscription = db_user.subscription
old_countries = subscription.connected_squads
added = [c for c in new_countries if c not in old_countries]
removed = [c for c in old_countries if c not in new_countries]
selected_countries = data.get('countries', [])
current_countries = subscription.connected_squads
added = [c for c in selected_countries if c not in current_countries]
removed = [c for c in current_countries if c not in selected_countries]
if not added and not removed:
await callback.answer("⚠️ Изменения не обнаружены", show_alert=True)
@@ -448,15 +467,17 @@ async def apply_countries_changes(
added_names = []
removed_names = []
added_server_prices = []
added_server_ids = []
for country in countries:
if country['uuid'] in added:
cost += country['price_kopeks']
added_names.append(country['name'])
added_server_prices.append(country['price_kopeks'])
if country['uuid'] in removed:
removed_names.append(country['name'])
texts = get_texts(db_user.language)
if cost > 0 and db_user.balance_kopeks < cost:
await callback.answer(
f"❌ Недостаточно средств!\nТребуется: {texts.format_price(cost)}\nУ вас: {texts.format_price(db_user.balance_kopeks)}",
@@ -465,7 +486,7 @@ async def apply_countries_changes(
return
try:
if cost > 0:
if added and cost > 0:
success = await subtract_user_balance(
db, db_user, cost,
f"Добавление стран: {', '.join(added_names)}"
@@ -482,7 +503,19 @@ async def apply_countries_changes(
description=f"Добавление стран к подписке: {', '.join(added_names)}"
)
subscription.connected_squads = new_countries
if added:
from app.database.crud.server_squad import get_server_ids_by_uuids, add_user_to_servers
from app.database.crud.subscription import add_subscription_servers
added_server_ids = await get_server_ids_by_uuids(db, added)
if added_server_ids:
await add_subscription_servers(db, subscription, added_server_ids, added_server_prices)
await add_user_to_servers(db, added_server_ids)
logger.info(f"📊 Добавлены серверы с ценами: {list(zip(added_server_ids, added_server_prices))}")
subscription.connected_squads = selected_countries
subscription.updated_at = datetime.utcnow()
await db.commit()
@@ -516,7 +549,7 @@ async def apply_countries_changes(
success_text += "\n".join(f"{name}" for name in removed_names)
success_text += "\nℹ️ Повторное подключение будет платным\n"
success_text += f"\n🌍 <b>Активных стран:</b> {len(new_countries)}"
success_text += f"\n🌍 <b>Активных стран:</b> {len(selected_countries)}"
await callback.message.edit_text(
success_text,
@@ -608,11 +641,27 @@ async def handle_extend_subscription(
await callback.answer("❌ Продление доступно за 3 дня до окончания подписки", show_alert=True)
return
subscription_service = SubscriptionService()
renewal_prices = {}
for days in [30, 90, 180]:
price = await subscription_service.calculate_renewal_price(subscription, days, db)
renewal_prices[days] = price
await callback.message.edit_text(
f"⏰ <b>Продление подписки</b>\n\n"
f"Осталось дней: {subscription.days_left}\n"
f"Выберите период продления:",
reply_markup=get_extend_subscription_keyboard(db_user.language)
f"Осталось дней: {subscription.days_left}\n\n"
f"<b>Ваша текущая конфигурация:</b>\n"
f"🌍 Серверов: {len(subscription.connected_squads)}\n"
f"📊 Трафик: {texts.format_traffic(subscription.traffic_limit_gb)}\n"
f"📱 Устройств: {subscription.device_limit}\n\n"
f"<b>Выберите период продления:</b>\n"
f"📅 30 дней - {texts.format_price(renewal_prices[30])}\n"
f"📅 90 дней - {texts.format_price(renewal_prices[90])}\n"
f"📅 180 дней - {texts.format_price(renewal_prices[180])}\n\n"
f"💡 <i>Цена включает все ваши текущие серверы и настройки</i>",
reply_markup=get_extend_subscription_keyboard_with_prices(db_user.language, renewal_prices),
parse_mode="HTML"
)
await callback.answer()
@@ -814,7 +863,8 @@ async def confirm_extend_subscription(
texts = get_texts(db_user.language)
subscription = db_user.subscription
price = PERIOD_PRICES[days]
subscription_service = SubscriptionService()
price = await subscription_service.calculate_renewal_price(subscription, days, db)
if db_user.balance_kopeks < price:
await callback.answer("❌ Недостаточно средств на балансе", show_alert=True)
@@ -859,11 +909,12 @@ async def confirm_extend_subscription(
await callback.message.edit_text(
f"✅ Подписка успешно продлена!\n\n"
f"⏰ Добавлено: {days} дней\n"
f"Действует до: {subscription.end_date.strftime('%d.%m.%Y %H:%M')}",
f"Действует до: {subscription.end_date.strftime('%d.%m.%Y %H:%M')}\n\n"
f"💰 Списано: {texts.format_price(price)}",
reply_markup=get_back_keyboard(db_user.language)
)
logger.info(f"✅ Пользователь {db_user.telegram_id} продлил подписку на {days} дней")
logger.info(f"✅ Пользователь {db_user.telegram_id} продлил подписку на {days} дней за {price/100}")
except Exception as e:
logger.error(f"Ошибка продления подписки: {e}")
@@ -875,6 +926,34 @@ async def confirm_extend_subscription(
await callback.answer()
def get_extend_subscription_keyboard_with_prices(language: str, prices: dict) -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text=f"📅 30 дней - {texts.format_price(prices[30])}",
callback_data="extend_period_30"
)
],
[
InlineKeyboardButton(
text=f"📅 90 дней - {texts.format_price(prices[90])}",
callback_data="extend_period_90"
)
],
[
InlineKeyboardButton(
text=f"📅 180 дней - {texts.format_price(prices[180])}",
callback_data="extend_period_180"
)
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
])
async def confirm_reset_traffic(
callback: types.CallbackQuery,
db_user: User,
@@ -994,7 +1073,8 @@ async def select_traffic(
async def select_country(
callback: types.CallbackQuery,
state: FSMContext,
db_user: User
db_user: User,
db: AsyncSession
):
country_uuid = callback.data.split('_')[1]
@@ -1010,10 +1090,12 @@ async def select_country(
base_price = PERIOD_PRICES[data['period_days']] + TRAFFIC_PRICES[data['traffic_gb']]
countries_price = 0
for country in countries:
if country['uuid'] in selected_countries:
countries_price += country['price_kopeks']
try:
subscription_service = SubscriptionService()
countries_price, _ = await subscription_service.get_countries_price_by_uuids(selected_countries, db)
except AttributeError:
logger.warning("Используем fallback функцию для расчета цен стран")
countries_price, _ = await get_countries_price_by_uuids_fallback(selected_countries, db)
data['countries'] = selected_countries
data['total_price'] = base_price + countries_price
@@ -1090,7 +1172,8 @@ async def select_devices(
async def devices_continue(
callback: types.CallbackQuery,
state: FSMContext,
db_user: User
db_user: User,
db: AsyncSession
):
if not callback.data == "devices_continue":
@@ -1101,17 +1184,32 @@ async def devices_continue(
texts = get_texts(db_user.language)
countries = await _get_available_countries()
selected_countries_names = [
c['name'] for c in countries
if c['uuid'] in data['countries']
]
selected_countries_names = []
try:
subscription_service = SubscriptionService()
countries_price, _ = await subscription_service.get_countries_price_by_uuids(data['countries'], db)
except AttributeError:
logger.warning("Используем fallback функцию для расчета цен стран")
countries_price, _ = await get_countries_price_by_uuids_fallback(data['countries'], db)
for country in countries:
if country['uuid'] in data['countries']:
selected_countries_names.append(country['name'])
base_price = PERIOD_PRICES[data['period_days']] + TRAFFIC_PRICES[data['traffic_gb']]
devices_price = (data['devices'] - 1) * settings.PRICE_PER_DEVICE
total_price = base_price + countries_price + devices_price
data['total_price'] = total_price
await state.set_data(data)
summary_text = texts.SUBSCRIPTION_SUMMARY.format(
period=data['period_days'],
traffic=texts.format_traffic(data['traffic_gb']),
countries=", ".join(selected_countries_names),
devices=data['devices'],
total_price=texts.format_price(data['total_price'])
total_price=texts.format_price(total_price)
)
await callback.message.edit_text(
@@ -1137,9 +1235,11 @@ async def confirm_purchase(
base_price = PERIOD_PRICES[data['period_days']] + TRAFFIC_PRICES[data['traffic_gb']]
countries_price = 0
server_prices = []
for country in countries:
if country['uuid'] in data['countries']:
countries_price += country['price_kopeks']
server_prices.append(country['price_kopeks'])
devices_price = (data['devices'] - 1) * settings.PRICE_PER_DEVICE
final_price = base_price + countries_price + devices_price
@@ -1207,20 +1307,10 @@ async def confirm_purchase(
server_ids = await get_server_ids_by_uuids(db, data['countries'])
if server_ids:
countries = await _get_available_countries()
server_prices = []
for country_uuid in data['countries']:
for country in countries:
if country['uuid'] == country_uuid:
server_prices.append(country['price_kopeks'])
break
else:
server_prices.append(0)
await add_subscription_servers(db, subscription, server_ids, server_prices)
await add_user_to_servers(db, server_ids)
logger.info(f"📊 Сохранены цены серверов: {server_prices}")
logger.info(f"📊 Обновлены счетчики пользователей для серверов: {server_ids}")
await db.refresh(db_user)
@@ -1257,11 +1347,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:
@@ -1282,6 +1384,38 @@ async def confirm_purchase(
await state.clear()
await callback.answer()
async def handle_subscription_settings(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
subscription = db_user.subscription
if not subscription or subscription.is_trial:
await callback.answer("⚠️ Настройки доступны только для платных подписок", show_alert=True)
return
devices_used = await get_current_devices_count(db_user)
settings_text = f"""
<b>Настройки подписки</b>
📊 <b>Текущие параметры:</b>
🌍 Стран: {len(subscription.connected_squads)}
📈 Трафик: {texts.format_traffic(subscription.traffic_used_gb)} / {texts.format_traffic(subscription.traffic_limit_gb)}
📱 Устройства: {devices_used} / {subscription.device_limit}
Выберите что хотите изменить:
"""
await callback.message.edit_text(
settings_text,
reply_markup=get_subscription_settings_keyboard(db_user.language),
parse_mode="HTML"
)
await callback.answer()
async def handle_autopay_menu(
callback: types.CallbackQuery,
@@ -1800,6 +1934,330 @@ 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
connect_mode = settings.CONNECT_BUTTON_MODE
if connect_mode == "miniapp_subscription":
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="🔗 Открыть подписку",
web_app=types.WebAppInfo(url=subscription.subscription_url)
)
],
[
InlineKeyboardButton(text="📋 Показать ссылку", callback_data="open_subscription_link")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
])
await callback.message.edit_text(
f"""
🔗 <b>Подключить подписку</b>
📱 Нажмите кнопку ниже, чтобы открыть подписку в мини-приложении Telegram:
""",
reply_markup=keyboard,
parse_mode="HTML"
)
elif connect_mode == "miniapp_custom":
if not settings.MINIAPP_CUSTOM_URL:
await callback.answer("❌ Кастомная ссылка для мини-приложения не настроена", show_alert=True)
return
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="🚀 Открыть приложение",
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL)
)
],
[
InlineKeyboardButton(text="📋 Показать ссылку подписки", callback_data="open_subscription_link")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
])
await callback.message.edit_text(
f"""
🚀 <b>Подключить подписку</b>
📱 Нажмите кнопку ниже, чтобы открыть приложение:
📋 <b>Ссылка подписки:</b>
<code>{subscription.subscription_url}</code>
""",
reply_markup=keyboard,
parse_mode="HTML"
)
else:
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
link_text = f"""
🔗 <b>Ссылка подписки:</b>
<code>{subscription.subscription_url}</code>
📱 <b>Как использовать:</b>
1. Нажмите на ссылку выше чтобы её скопировать
2. Откройте ваше VPN приложение
3. Найдите функцию "Добавить подписку" или "Import"
4. Вставьте скопированную ссылку
💡 Если ссылка не скопировалась, выделите её вручную и скопируйте.
"""
await callback.message.edit_text(
link_text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
]),
parse_mode="HTML"
)
await callback.answer()
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 +2434,34 @@ 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"
)
dp.callback_query.register(
handle_subscription_settings,
F.data == "subscription_settings"
)
+171
View File
@@ -0,0 +1,171 @@
import logging
from datetime import datetime, timedelta
from typing import List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest
from app.database.models import User, Subscription
from app.database.crud.user import get_user_by_id, subtract_user_balance
from app.database.crud.subscription import get_expiring_subscriptions, extend_subscription
from app.database.crud.transaction import create_transaction
from app.database.models import TransactionType
from app.keyboards.inline import get_autopay_notification_keyboard, get_subscription_expiring_keyboard
from app.localization.texts import get_texts
from app.services.subscription_service import SubscriptionService
logger = logging.getLogger(__name__)
async def send_subscription_expiring_notification(
bot: Bot,
db: AsyncSession,
subscription: Subscription,
days_left: int
) -> bool:
try:
user = await get_user_by_id(db, subscription.user_id)
if not user:
return False
texts = get_texts(user.language)
if subscription.is_trial:
text = texts.TRIAL_ENDING_SOON.format(
price=texts.format_price(30000)
)
else:
autopay_status = texts.AUTOPAY_ENABLED_TEXT if subscription.autopay_enabled else texts.AUTOPAY_DISABLED_TEXT
if subscription.autopay_enabled:
action_text = f"💰 Убедитесь, что на балансе достаточно средств: {texts.format_price(user.balance_kopeks)}"
else:
action_text = "💡 Включите автоплатеж или продлите подписку вручную"
text = texts.SUBSCRIPTION_EXPIRING_PAID.format(
days=days_left,
end_date=subscription.end_date.strftime("%d.%m.%Y %H:%M"),
autopay_status=autopay_status,
action_text=action_text
)
keyboard = get_subscription_expiring_keyboard(subscription.id, user.language)
await bot.send_message(
chat_id=user.telegram_id,
text=text,
reply_markup=keyboard,
parse_mode="HTML"
)
logger.info(f"✅ Отправлено уведомление об истечении подписки пользователю {user.telegram_id}")
return True
except TelegramBadRequest as e:
logger.warning(f"⚠️ Не удалось отправить уведомление пользователю {user.telegram_id}: {e}")
return False
except Exception as e:
logger.error(f"❌ Ошибка отправки уведомления об истечении подписки: {e}")
return False
async def send_autopay_failed_notification(
bot: Bot,
db: AsyncSession,
subscription: Subscription,
required_amount: int
) -> bool:
try:
user = await get_user_by_id(db, subscription.user_id)
if not user:
return False
texts = get_texts(user.language)
text = texts.AUTOPAY_FAILED.format(
balance=texts.format_price(user.balance_kopeks),
required=texts.format_price(required_amount)
)
keyboard = get_autopay_notification_keyboard(subscription.id, user.language)
await bot.send_message(
chat_id=user.telegram_id,
text=text,
reply_markup=keyboard,
parse_mode="HTML"
)
logger.info(f"✅ Отправлено уведомление о неудачном автоплатеже пользователю {user.telegram_id}")
return True
except TelegramBadRequest as e:
logger.warning(f"⚠️ Не удалось отправить уведомление пользователю {user.telegram_id}: {e}")
return False
except Exception as e:
logger.error(f"❌ Ошибка отправки уведомления о неудачном автоплатеже: {e}")
return False
async def process_autopayment(
bot: Bot,
db: AsyncSession,
subscription: Subscription
) -> bool:
try:
user = await get_user_by_id(db, subscription.user_id)
if not user:
logger.error(f"Пользователь {subscription.user_id} не найден для автоплатежа")
return False
subscription_service = SubscriptionService()
renewal_cost = await subscription_service.calculate_renewal_price(
subscription, 30, db
)
if user.balance_kopeks < renewal_cost:
logger.warning(f"Недостаточно средств для автоплатежа у пользователя {user.telegram_id}")
await send_autopay_failed_notification(bot, db, subscription, renewal_cost)
return False
success = await subtract_user_balance(
db, user, renewal_cost,
f"Автопродление подписки на 30 дней"
)
if not success:
logger.error(f"Ошибка списания средств для автоплатежа у пользователя {user.telegram_id}")
await send_autopay_failed_notification(bot, db, subscription, renewal_cost)
return False
await extend_subscription(db, subscription, 30)
await subscription_service.update_remnawave_user(db, subscription)
await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=renewal_cost,
description="Автопродление подписки на 30 дней"
)
texts = get_texts(user.language)
success_text = texts.AUTOPAY_SUCCESS.format(
days=30,
amount=texts.format_price(renewal_cost),
new_end_date=subscription.end_date.strftime("%d.%m.%Y %H:%M")
)
await bot.send_message(
chat_id=user.telegram_id,
text=success_text,
parse_mode="HTML"
)
logger.info(f"✅ Автоплатеж успешно выполнен для пользователя {user.telegram_id}")
return True
except Exception as e:
logger.error(f"❌ Ошибка обработки автоплатежа: {e}")
return False
+69 -21
View File
@@ -172,24 +172,38 @@ def get_admin_statistics_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
])
def get_user_management_keyboard(user_id: int, language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
def get_user_management_keyboard(user_id: int, user_status: str, language: str = "ru") -> InlineKeyboardMarkup:
keyboard = [
[
InlineKeyboardButton(text="💰 Баланс", callback_data=f"admin_user_balance_{user_id}"),
InlineKeyboardButton(text="📱 Подписка", callback_data=f"admin_user_sub_{user_id}")
],
[
InlineKeyboardButton(text="📊 Статистика", callback_data=f"admin_user_stats_{user_id}"),
InlineKeyboardButton(text="📋 Транзакции", callback_data=f"admin_user_trans_{user_id}")
InlineKeyboardButton(text="📱 Подписка", callback_data=f"admin_user_subscription_{user_id}")
],
[
InlineKeyboardButton(text="📊 Статистика", callback_data=f"admin_user_statistics_{user_id}"),
InlineKeyboardButton(text="📋 Транзакции", callback_data=f"admin_user_transactions_{user_id}")
]
]
if user_status == "active":
keyboard.append([
InlineKeyboardButton(text="🚫 Заблокировать", callback_data=f"admin_user_block_{user_id}"),
InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"admin_user_delete_{user_id}")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_users_list")
]
])
elif user_status == "blocked":
keyboard.append([
InlineKeyboardButton(text="✅ Разблокировать", callback_data=f"admin_user_unblock_{user_id}"),
InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"admin_user_delete_{user_id}")
])
elif user_status == "deleted":
keyboard.append([
InlineKeyboardButton(text="❌ Пользователь удален", callback_data="noop")
])
keyboard.append([
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_users_list")
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_confirmation_keyboard(
@@ -293,8 +307,8 @@ def get_custom_criteria_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
InlineKeyboardButton(text="⚡ Активные сегодня", callback_data="criteria_active_today")
],
[
InlineKeyboardButton(text="💤 Неактивные 7+ дней", callback_data="criteria_inactive_week"),
InlineKeyboardButton(text="💤 Неактивные 30+ дней", callback_data="criteria_inactive_month")
InlineKeyboardButton(text="👤 Неактивные 7+ дней", callback_data="criteria_inactive_week"),
InlineKeyboardButton(text="👤 Неактивные 30+ дней", callback_data="criteria_inactive_month")
],
[
InlineKeyboardButton(text="🤝 Через рефералов", callback_data="criteria_referrals"),
@@ -339,18 +353,52 @@ def get_broadcast_history_keyboard(page: int, total_pages: int, language: str =
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_sync_options_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
keyboard = [
[InlineKeyboardButton(text="🔄 Полная синхронизация", callback_data="sync_all_users")],
[InlineKeyboardButton(text="🆕 Только новые", callback_data="sync_new_users")],
[InlineKeyboardButton(text="📈 Обновить данные", callback_data="sync_update_data")],
[
InlineKeyboardButton(text="🔄 Синхронизировать всех", callback_data="sync_all_users"),
InlineKeyboardButton(text=" Только новых", callback_data="sync_new_users")
InlineKeyboardButton(text="🔍 Валидация", callback_data="sync_validate"),
InlineKeyboardButton(text="🧹 Очистка", callback_data="sync_cleanup")
],
[InlineKeyboardButton(text="💡 Рекомендации", callback_data="sync_recommendations")],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")]
]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_sync_confirmation_keyboard(sync_type: str, language: str = "ru") -> InlineKeyboardMarkup:
keyboard = [
[InlineKeyboardButton(text="✅ Подтвердить", callback_data=f"confirm_{sync_type}")],
[InlineKeyboardButton(text="❌ Отмена", callback_data="admin_rw_sync")]
]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_sync_result_keyboard(sync_type: str, has_errors: bool = False, language: str = "ru") -> InlineKeyboardMarkup:
keyboard = []
if has_errors:
keyboard.append([
InlineKeyboardButton(text="🔄 Повторить", callback_data=f"sync_{sync_type}")
])
if sync_type != "all_users":
keyboard.append([
InlineKeyboardButton(text="🔄 Полная синхронизация", callback_data="sync_all_users")
])
keyboard.extend([
[
InlineKeyboardButton(text="📊 Обновить данные", callback_data="sync_update_data")
InlineKeyboardButton(text="📊 Статистика", callback_data="admin_rw_system"),
InlineKeyboardButton(text="🔍 Валидация", callback_data="sync_validate")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_remnawave")
]
[InlineKeyboardButton(text="⬅️ К синхронизации", callback_data="admin_rw_sync")],
[InlineKeyboardButton(text="🏠 В главное меню", callback_data="admin_remnawave")]
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_period_selection_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
@@ -526,4 +574,4 @@ def get_admin_pagination_keyboard(
InlineKeyboardButton(text="⬅️ Назад", callback_data=back_callback)
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
return InlineKeyboardMarkup(inline_keyboard=keyboard)
+300 -52
View File
@@ -97,44 +97,44 @@ def get_subscription_keyboard(
is_trial: bool = False,
subscription=None
) -> InlineKeyboardMarkup:
from app.config import settings
texts = get_texts(language)
keyboard = []
if has_subscription:
if subscription and subscription.subscription_url:
connect_mode = settings.CONNECT_BUTTON_MODE
if connect_mode == "miniapp_subscription":
button_text = "🚀 Подключить подписку"
elif connect_mode == "miniapp_custom":
button_text = "🚀 Подключить подписку"
else:
button_text = "🔗 Подключиться"
keyboard.append([
InlineKeyboardButton(text=button_text, callback_data="subscription_connect")
])
if not is_trial and subscription and subscription.days_left <= 3:
keyboard.append([
InlineKeyboardButton(text="⏰ Продлить", callback_data="subscription_extend")
])
if not is_trial:
keyboard.append([
InlineKeyboardButton(text="💳 Автоплатеж", callback_data="subscription_autopay")
])
if is_trial:
keyboard.append([
InlineKeyboardButton(text=texts.MENU_BUY_SUBSCRIPTION, callback_data="subscription_upgrade")
])
else:
row1 = []
row2 = []
row3 = []
row4 = []
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"))
row2.append(InlineKeyboardButton(text="📱 Добавить устройства", callback_data="subscription_add_devices"))
if subscription and subscription.days_left <= 3:
row2.append(InlineKeyboardButton(text="⏰ Продлить", callback_data="subscription_extend"))
row3.append(InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data="subscription_reset_traffic"))
row3.append(InlineKeyboardButton(text="💳 Автоплатеж", callback_data="subscription_autopay"))
row4.append(InlineKeyboardButton(text="🔄 Сбросить устройства", callback_data="subscription_reset_devices"))
if row1:
keyboard.append(row1)
if row2:
keyboard.append(row2)
if row3:
keyboard.append(row3)
if row4:
keyboard.append(row4)
keyboard.append([
InlineKeyboardButton(text="⚙️ Настройки подписки", callback_data="subscription_settings")
])
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
@@ -142,6 +142,31 @@ def get_subscription_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_subscription_settings_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = [
[
InlineKeyboardButton(text="🌍 Добавить страны", callback_data="subscription_add_countries")
],
[
InlineKeyboardButton(text="📈 Добавить трафик", callback_data="subscription_add_traffic")
],
[
InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data="subscription_reset_traffic")
],
[
InlineKeyboardButton(text="📱 Добавить устройства", callback_data="subscription_add_devices")
],
[
InlineKeyboardButton(text="🔄 Сбросить устройства", callback_data="subscription_reset_devices")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_trial_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
@@ -189,7 +214,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:
@@ -278,46 +303,95 @@ def get_subscription_confirm_keyboard(language: str = "ru") -> InlineKeyboardMar
def get_balance_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
keyboard = []
keyboard.append([
InlineKeyboardButton(text=texts.BALANCE_HISTORY, callback_data="balance_history"),
InlineKeyboardButton(text=texts.BALANCE_TOP_UP, callback_data="balance_topup")
])
if settings.TRIBUTE_ENABLED:
keyboard.append([
InlineKeyboardButton(text="💳 Быстрое пополнение", callback_data="tribute_quick_pay")
])
keyboard.append([
InlineKeyboardButton(text=texts.BALANCE_SUPPORT_REQUEST, callback_data="balance_support")
])
# Назад
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
])
keyboard = [
[
InlineKeyboardButton(text=texts.BALANCE_HISTORY, callback_data="balance_history"),
InlineKeyboardButton(text=texts.BALANCE_TOP_UP, callback_data="balance_topup")
],
[
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
]
]
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> InlineKeyboardMarkup:
"""Клавиатура выбора способа оплаты"""
texts = get_texts(language)
keyboard = []
if settings.TELEGRAM_STARS_ENABLED:
keyboard.append([
InlineKeyboardButton(text=texts.TOP_UP_STARS, callback_data=f"pay_stars_{amount_kopeks}")
InlineKeyboardButton(
text="⭐ Telegram Stars",
callback_data="topup_stars"
)
])
if settings.TRIBUTE_ENABLED:
keyboard.append([
InlineKeyboardButton(
text="💳 Банковская карта",
callback_data="topup_tribute"
)
])
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")
InlineKeyboardButton(
text="🛠️ Через поддержку",
callback_data="topup_support"
)
])
keyboard.append([
InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance")
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_autopay_notification_keyboard(subscription_id: int, language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="balance_topup"
)
],
[
InlineKeyboardButton(
text="📱 Моя подписка",
callback_data="menu_subscription"
)
]
])
def get_subscription_expiring_keyboard(subscription_id: int, language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="⏰ Продлить подписку",
callback_data="subscription_extend"
)
],
[
InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="balance_topup"
)
],
[
InlineKeyboardButton(
text="📱 Моя подписка",
callback_data="menu_subscription"
)
]
])
def get_referral_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
@@ -534,7 +608,6 @@ def get_manage_countries_keyboard(
current_subscription_countries: List[str],
language: str = "ru"
) -> InlineKeyboardMarkup:
"""Клавиатура для управления странами подписки БЕЗ состояний FSM"""
texts = get_texts(language)
keyboard = []
@@ -592,3 +665,178 @@ def get_manage_countries_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_device_selection_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
from app.config import settings
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")
]
]
if settings.CONNECT_BUTTON_MODE == "guide":
keyboard.append([
InlineKeyboardButton(text="📋 Показать ссылку подписки", callback_data="open_subscription_link")
])
keyboard.append([
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_connection_guide_keyboard(
subscription_url: str,
app: dict,
language: str = "ru"
) -> InlineKeyboardMarkup:
from app.handlers.subscription import create_deep_link
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:
from app.handlers.subscription import create_deep_link
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)
def get_extend_subscription_keyboard_with_prices(language: str, prices: dict) -> InlineKeyboardMarkup:
from app.localization.texts import get_texts
texts = get_texts(language)
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text=f"📅 30 дней - {texts.format_price(prices[30])}",
callback_data="extend_period_30"
)
],
[
InlineKeyboardButton(
text=f"📅 90 дней - {texts.format_price(prices[90])}",
callback_data="extend_period_90"
)
],
[
InlineKeyboardButton(
text=f"📅 180 дней - {texts.format_price(prices[180])}",
callback_data="extend_period_180"
)
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
])
+6 -6
View File
@@ -106,11 +106,11 @@ class RussianTexts(Texts):
REFERRAL_CODE_INVALID = "❌ Неверный реферальный код"
REFERRAL_CODE_SKIP = "⏭️ Пропустить"
MAIN_MENU = """
👤 <b>{user_name}</b>
MAIN_MENU = """👤 <b>{user_name}</b>
💰 <b>Баланс:</b> {balance}
📱 <b>Подписка:</b> {subscription_status}
Выберите действие:
"""
@@ -268,7 +268,7 @@ class RussianTexts(Texts):
TRIAL_ENDING_SOON = """
🎁 <b>Тестовая подписка скоро закончится!</b>
Ваша тестовая подписка истекает через 2 часа.
Ваша тестовая подписка истекает через несколько часов.
💎 <b>Не хотите остаться без VPN?</b>
Переходите на полную подписку!
@@ -354,7 +354,7 @@ class RussianTexts(Texts):
ADMIN_MONITORING = "🔍 Мониторинг"
ADMIN_REFERRALS = "🤝 Рефералы"
ADMIN_RULES = "📋 Правила"
ADMIN_REMNAWAVE = "🖥️ RemnaWave"
ADMIN_REMNAWAVE = "🖥️ Remnawave"
ADMIN_STATISTICS = "📊 Статистика"
ACCESS_DENIED = "❌ Доступ запрещен"
@@ -485,4 +485,4 @@ async def refresh_rules_cache(language: str = "ru"):
def clear_rules_cache():
global _cached_rules
_cached_rules.clear()
print("✅ Кеш правил очищен")
print("✅ Кеш правил очищен")
+56 -6
View File
@@ -56,7 +56,7 @@ class AuthMiddleware(BaseMiddleware):
)
if is_registration_process:
logger.info(f"🔓 Пропускаем пользователя {user.id} в процессе регистрации")
logger.info(f"📝 Пропускаем пользователя {user.id} в процессе регистрации")
data['db'] = db
data['db_user'] = None
data['is_admin'] = False
@@ -64,24 +64,74 @@ class AuthMiddleware(BaseMiddleware):
else:
if isinstance(event, Message):
await event.answer(
"️ Для начала работы необходимо выполнить команду /start"
"️ Для начала работы необходимо выполнить команду /start"
)
elif isinstance(event, CallbackQuery):
await event.answer(
"️ Необходимо начать с команды /start",
"️ Необходимо начать с команды /start",
show_alert=True
)
logger.info(f"🚫 Заблокирован незарегистрированный пользователь {user.id}")
return
else:
from app.database.models import UserStatus
if db_user.status == UserStatus.BLOCKED.value:
if isinstance(event, Message):
await event.answer("🚫 Ваш аккаунт заблокирован администратором.")
elif isinstance(event, CallbackQuery):
await event.answer("🚫 Ваш аккаунт заблокирован администратором.", show_alert=True)
logger.info(f"🚫 Заблокированный пользователь {user.id} попытался использовать бота")
return
if db_user.status == UserStatus.DELETED.value:
state: FSMContext = data.get('state')
current_state = None
if state:
current_state = await state.get_state()
registration_states = [
RegistrationStates.waiting_for_rules_accept,
RegistrationStates.waiting_for_referral_code
]
is_start_or_registration = (
(isinstance(event, Message) and event.text and event.text.startswith('/start'))
or (isinstance(event, CallbackQuery) and current_state and
any(str(state) in str(current_state) for state in registration_states))
or (isinstance(event, CallbackQuery) and event.data and
(event.data in ['rules_accept', 'rules_decline', 'referral_skip']))
)
if is_start_or_registration:
logger.info(f"🔄 Удаленный пользователь {user.id} начинает повторную регистрацию")
data['db'] = db
data['db_user'] = None
data['is_admin'] = False
return await handler(event, data)
else:
if isinstance(event, Message):
await event.answer(
"❌ Ваш аккаунт был удален.\n"
"🔄 Для повторной регистрации выполните команду /start"
)
elif isinstance(event, CallbackQuery):
await event.answer(
"❌ Ваш аккаунт был удален. Для повторной регистрации выполните /start",
show_alert=True
)
logger.info(f"❌ Удаленный пользователь {user.id} попытался использовать бота без /start")
return
from datetime import datetime
db_user.last_activity = datetime.utcnow()
await db.commit()
data['db'] = db
data['db_user'] = db_user
data['is_admin'] = settings.is_admin(user.id)
return await handler(event, data)
except Exception as e:
@@ -90,4 +140,4 @@ class AuthMiddleware(BaseMiddleware):
if hasattr(event, 'data'):
logger.error(f"Callback data: {event.data}")
await db.rollback()
raise
raise
+77 -53
View File
@@ -32,7 +32,7 @@ class MonitoringService:
self.subscription_service = SubscriptionService()
self.payment_service = PaymentService()
self.bot = bot
self._notified_users: Set[str] = set() # Защита от дублирования уведомлений
self._notified_users: Set[str] = set()
async def start_monitoring(self):
if self.is_running:
@@ -60,12 +60,11 @@ class MonitoringService:
try:
await self._check_expired_subscriptions(db)
await self._check_expiring_subscriptions(db)
await self._check_trial_expiring_soon(db) # Новый метод!
await self._check_trial_expiring_soon(db)
await self._process_autopayments(db)
await self._cleanup_inactive_users(db)
await self._sync_with_remnawave(db)
# Очищаем кеш уведомлений каждые 24 часа
current_hour = datetime.utcnow().hour
if current_hour == 0:
self._notified_users.clear()
@@ -88,7 +87,6 @@ class MonitoringService:
break
async def _check_expired_subscriptions(self, db: AsyncSession):
"""Проверка истекших подписок"""
try:
expired_subscriptions = await get_expired_subscriptions(db)
@@ -99,7 +97,6 @@ class MonitoringService:
if user and user.remnawave_uuid:
await self.subscription_service.disable_remnawave_user(user.remnawave_uuid)
# Отправляем уведомление об истечении
if user and self.bot:
await self._send_subscription_expired_notification(user)
@@ -116,12 +113,10 @@ class MonitoringService:
logger.error(f"Ошибка проверки истекших подписок: {e}")
async def _check_expiring_subscriptions(self, db: AsyncSession):
"""Проверка подписок, истекающих через 2-3 дня (только платные)"""
try:
warning_days = settings.get_autopay_warning_days()
for days in warning_days:
# Получаем только платные подписки
expiring_subscriptions = await self._get_expiring_paid_subscriptions(db, days)
for subscription in expiring_subscriptions:
@@ -131,7 +126,7 @@ class MonitoringService:
notification_key = f"expiring_{user.telegram_id}_{days}d"
if notification_key in self._notified_users:
continue # Уже уведомляли сегодня
continue
if self.bot:
await self._send_subscription_expiring_notification(user, subscription, days)
@@ -150,9 +145,7 @@ class MonitoringService:
logger.error(f"Ошибка проверки истекающих подписок: {e}")
async def _check_trial_expiring_soon(self, db: AsyncSession):
"""Проверка тестовых подписок, истекающих через 2 часа"""
try:
# Получаем тестовые подписки, истекающие через 2 часа
threshold_time = datetime.utcnow() + timedelta(hours=2)
result = await db.execute(
@@ -176,7 +169,7 @@ class MonitoringService:
notification_key = f"trial_2h_{user.telegram_id}"
if notification_key in self._notified_users:
continue # Уже уведомляли
continue
if self.bot:
await self._send_trial_ending_notification(user, subscription)
@@ -195,7 +188,6 @@ class MonitoringService:
logger.error(f"Ошибка проверки истекающих тестовых подписок: {e}")
async def _get_expiring_paid_subscriptions(self, db: AsyncSession, days_before: int) -> List[Subscription]:
"""Получение платных подписок, истекающих через указанное количество дней"""
threshold_date = datetime.utcnow() + timedelta(days=days_before)
result = await db.execute(
@@ -204,7 +196,7 @@ class MonitoringService:
.where(
and_(
Subscription.status == SubscriptionStatus.ACTIVE.value,
Subscription.is_trial == False, # Только платные
Subscription.is_trial == False,
Subscription.end_date <= threshold_date,
Subscription.end_date > datetime.utcnow()
)
@@ -213,9 +205,7 @@ class MonitoringService:
return result.scalars().all()
async def _process_autopayments(self, db: AsyncSession):
"""Обработка автоплатежей"""
try:
# Исправленный запрос с использованием индивидуальных настроек
current_time = datetime.utcnow()
result = await db.execute(
@@ -225,13 +215,12 @@ class MonitoringService:
and_(
Subscription.status == SubscriptionStatus.ACTIVE.value,
Subscription.autopay_enabled == True,
Subscription.is_trial == False # Автооплата только для платных
Subscription.is_trial == False
)
)
)
all_autopay_subscriptions = result.scalars().all()
# Фильтруем по времени с учетом индивидуальных настроек
autopay_subscriptions = []
for sub in all_autopay_subscriptions:
days_before_expiry = (sub.end_date - current_time).days
@@ -248,24 +237,20 @@ class MonitoringService:
renewal_cost = settings.PRICE_30_DAYS
# Проверяем, не списывали ли уже сегодня
autopay_key = f"autopay_{user.telegram_id}_{subscription.id}"
if autopay_key in self._notified_users:
continue
if user.balance_kopeks >= renewal_cost:
# Списываем средства
success = await subtract_user_balance(
db, user, renewal_cost,
"Автопродление подписки"
)
if success:
# Продлеваем подписку
await extend_subscription(db, subscription, 30)
await self.subscription_service.update_remnawave_user(db, subscription)
# Уведомляем об успешном автоплатеже
if self.bot:
await self._send_autopay_success_notification(user, renewal_cost, 30)
@@ -279,7 +264,6 @@ class MonitoringService:
logger.warning(f"💳 Ошибка списания средств для автопродления пользователя {user.telegram_id}")
else:
failed_count += 1
# Уведомляем о недостатке средств
if self.bot:
await self._send_autopay_failed_notification(user, user.balance_kopeks, renewal_cost)
logger.warning(f"💳 Недостаточно средств для автопродления у пользователя {user.telegram_id}")
@@ -294,54 +278,98 @@ class MonitoringService:
except Exception as e:
logger.error(f"Ошибка обработки автоплатежей: {e}")
# Методы отправки уведомлений
async def _send_subscription_expired_notification(self, user: User):
"""Уведомление об истечении подписки"""
try:
texts = get_texts(user.language)
message = texts.SUBSCRIPTION_EXPIRED
await self.bot.send_message(user.telegram_id, message, parse_mode="HTML")
message = """
<b>Подписка истекла</b>
Ваша подписка истекла. Для восстановления доступа продлите подписку.
🔧 Доступ к серверам заблокирован до продления.
"""
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💎 Купить подписку", callback_data="menu_buy")],
[InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="balance_topup")]
])
await self.bot.send_message(
user.telegram_id,
message,
parse_mode="HTML",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления об истечении подписки пользователю {user.telegram_id}: {e}")
async def _send_subscription_expiring_notification(self, user: User, subscription: Subscription, days: int):
"""Уведомление об истечении подписки через N дней"""
try:
texts = get_texts(user.language)
message = texts.SUBSCRIPTION_EXPIRING.format(days=days)
await self.bot.send_message(user.telegram_id, message, parse_mode="HTML")
if subscription.autopay_enabled:
autopay_status = "✅ Включен - подписка продлится автоматически"
action_text = f"💰 Убедитесь, что на балансе достаточно средств: {texts.format_price(user.balance_kopeks)}"
else:
autopay_status = "❌ Отключен - не забудьте продлить вручную!"
action_text = "💡 Включите автоплатеж или продлите подписку вручную"
message = f"""
<b>Подписка истекает через {days} дней!</b>
Ваша платная подписка истекает {subscription.end_date.strftime("%d.%m.%Y %H:%M")}.
💳 <b>Автоплатеж:</b> {autopay_status}
{action_text}
"""
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⏰ Продлить подписку", callback_data="subscription_extend")],
[InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="balance_topup")],
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")]
])
await self.bot.send_message(
user.telegram_id,
message,
parse_mode="HTML",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления об истечении подписки пользователю {user.telegram_id}: {e}")
async def _send_trial_ending_notification(self, user: User, subscription: Subscription):
"""Уведомление об окончании тестовой подписки через 2 часа"""
try:
texts = get_texts(user.language)
# Создаем специальное сообщение для тестовой подписки
message = f"""
🎁 <b>Тестовая подписка скоро закончится!</b>
🎁 <b>Тестовая подписка скоро закончится!</b>
Ваша тестовая подписка истекает через 2 часа.
Ваша тестовая подписка истекает через 2 часа.
💎 <b>Не хотите остаться без VPN?</b>
Переходите на полную подписку со скидкой!
💎 <b>Не хотите остаться без VPN?</b>
Переходите на полную подписку со скидкой!
🔥 <b>Специальное предложение:</b>
30 дней всего за {settings.format_price(settings.PRICE_30_DAYS)}
Безлимитный трафик
Все серверы доступны
Поддержка до 3 устройств
🔥 <b>Специальное предложение:</b>
30 дней всего за {settings.format_price(settings.PRICE_30_DAYS)}
Безлимитный трафик
Все серверы доступны
Поддержка до 3 устройств
Успейте оформить до окончания тестового периода!
"""
Успейте оформить до окончания тестового периода!
"""
# Добавляем inline клавиатуру с кнопкой покупки
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💎 Купить подписку", callback_data="buy_subscription")],
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="balance_top_up")]
[InlineKeyboardButton(text="💎 Купить подписку", callback_data="menu_buy")],
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="balance_topup")]
])
await self.bot.send_message(
@@ -355,7 +383,6 @@ class MonitoringService:
logger.error(f"Ошибка отправки уведомления об окончании тестовой подписки пользователю {user.telegram_id}: {e}")
async def _send_autopay_success_notification(self, user: User, amount: int, days: int):
"""Уведомление об успешном автоплатеже"""
try:
texts = get_texts(user.language)
message = texts.AUTOPAY_SUCCESS.format(
@@ -367,7 +394,6 @@ class MonitoringService:
logger.error(f"Ошибка отправки уведомления об автоплатеже пользователю {user.telegram_id}: {e}")
async def _send_autopay_failed_notification(self, user: User, balance: int, required: int):
"""Уведомление о неудачном автоплатеже"""
try:
texts = get_texts(user.language)
message = texts.AUTOPAY_FAILED.format(
@@ -375,11 +401,11 @@ class MonitoringService:
required=settings.format_price(required)
)
# Добавляем кнопку пополнения баланса
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="balance_top_up")]
[InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="balance_topup")],
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")]
])
await self.bot.send_message(
@@ -392,7 +418,6 @@ class MonitoringService:
except Exception as e:
logger.error(f"Ошибка отправки уведомления о неудачном автоплатеже пользователю {user.telegram_id}: {e}")
# Остальные методы остаются без изменений...
async def _cleanup_inactive_users(self, db: AsyncSession):
try:
now = datetime.utcnow()
@@ -523,7 +548,6 @@ class MonitoringService:
async def force_check_subscriptions(self, db: AsyncSession) -> Dict[str, int]:
try:
# Проверяем истекшие
expired_subscriptions = await get_expired_subscriptions(db)
expired_count = 0
@@ -618,4 +642,4 @@ class MonitoringService:
return 0
monitoring_service = MonitoringService()
monitoring_service = MonitoringService()
+374 -10
View File
@@ -391,17 +391,34 @@ class RemnaWaveService:
return False
async def sync_users_from_panel(self, db: AsyncSession, sync_type: str = "all") -> Dict[str, int]:
"""
Синхронизация пользователей из панели RemnaWave в бота
sync_type:
- "all": полная синхронизация (создание + обновление + удаление)
- "new_only": только создание новых пользователей
- "update_only": только обновление существующих
"""
try:
stats = {"created": 0, "updated": 0, "errors": 0}
stats = {"created": 0, "updated": 0, "errors": 0, "deleted": 0}
logger.info(f"🔄 Начинаем синхронизацию типа: {sync_type}")
async with self.api as api:
# Получаем всех пользователей из панели
panel_users_data = await api._make_request('GET', '/api/users')
panel_users = panel_users_data['response']['users']
logger.info(f"👥 Найдено пользователей в панели: {len(panel_users)}")
# Получаем всех пользователей из бота для сравнения
bot_users = await get_users_list(db, offset=0, limit=10000)
bot_users_by_telegram_id = {user.telegram_id: user for user in bot_users}
# Множество telegram_id из панели для проверки удаленных
panel_telegram_ids = set()
# Обрабатываем каждого пользователя из панели
for i, panel_user in enumerate(panel_users):
try:
telegram_id = panel_user.get('telegramId')
@@ -409,13 +426,16 @@ class RemnaWaveService:
logger.debug(f"➡️ Пропускаем пользователя без telegram_id")
continue
panel_telegram_ids.add(telegram_id)
logger.info(f"🔄 Обрабатываем пользователя {i+1}/{len(panel_users)}: {telegram_id}")
db_user = await get_user_by_telegram_id(db, telegram_id)
db_user = bot_users_by_telegram_id.get(telegram_id)
if not db_user:
# Пользователя нет в боте - создаем
if sync_type in ["new_only", "all"]:
logger.info(f"📝 Создание пользователя для telegram_id {telegram_id}")
logger.info(f"🆕 Создание пользователя для telegram_id {telegram_id}")
from app.database.crud.user import create_user
@@ -435,6 +455,7 @@ class RemnaWaveService:
logger.info(f"✅ Создан пользователь {telegram_id} с подпиской")
else:
# Пользователь есть в боте - обновляем
if sync_type in ["update_only", "all"]:
logger.debug(f"🔄 Обновление пользователя {telegram_id}")
@@ -450,13 +471,33 @@ class RemnaWaveService:
logger.error(f"❌ Ошибка обработки пользователя {telegram_id}: {user_error}")
stats["errors"] += 1
continue
# Удаляем подписки пользователей, которых нет в панели
if sync_type == "all":
logger.info("🗑️ Удаляем подписки пользователей, отсутствующих в панели...")
for telegram_id, db_user in bot_users_by_telegram_id.items():
if telegram_id not in panel_telegram_ids and db_user.subscription:
try:
logger.info(f"🗑️ Удаляем подписку пользователя {telegram_id} (нет в панели)")
# Деактивируем подписку
from app.database.crud.subscription import deactivate_subscription
await deactivate_subscription(db, db_user.subscription)
stats["deleted"] += 1
logger.info(f"✅ Деактивирована подписка пользователя {telegram_id}")
except Exception as delete_error:
logger.error(f"❌ Ошибка удаления подписки {telegram_id}: {delete_error}")
stats["errors"] += 1
logger.info(f"🎯 Синхронизация завершена: создано {stats['created']}, обновлено {stats['updated']}, ошибок {stats['errors']}")
logger.info(f"🎯 Синхронизация завершена: создано {stats['created']}, обновлено {stats['updated']}, удалено {stats['deleted']}, ошибок {stats['errors']}")
return stats
except Exception as e:
logger.error(f"❌ Критическая ошибка синхронизации пользователей: {e}")
return {"created": 0, "updated": 0, "errors": 1}
return {"created": 0, "updated": 0, "errors": 1, "deleted": 0}
async def _create_subscription_from_panel_data(self, db: AsyncSession, user, panel_user):
try:
@@ -549,6 +590,7 @@ class RemnaWaveService:
async def _update_subscription_from_panel_data(self, db: AsyncSession, user, panel_user):
try:
from app.database.crud.subscription import get_subscription_by_user_id
from app.database.models import SubscriptionStatus
from datetime import datetime, timedelta
subscription = await get_subscription_by_user_id(db, user.id)
@@ -557,18 +599,72 @@ class RemnaWaveService:
await self._create_subscription_from_panel_data(db, user, panel_user)
return
# Обновляем статус подписки
panel_status = panel_user.get('status', 'ACTIVE')
expire_at_str = panel_user.get('expireAt', '')
try:
if expire_at_str:
if expire_at_str.endswith('Z'):
expire_at_str = expire_at_str[:-1] + '+00:00'
expire_at = datetime.fromisoformat(expire_at_str)
if expire_at.tzinfo is not None:
expire_at = expire_at.replace(tzinfo=None)
# Обновляем дату окончания если она отличается
if abs((subscription.end_date - expire_at).total_seconds()) > 60: # больше минуты разницы
subscription.end_date = expire_at
logger.debug(f"Обновлена дата окончания подписки до {expire_at}")
except Exception as date_error:
logger.warning(f"⚠️ Ошибка парсинга даты при обновлении {expire_at_str}: {date_error}")
# Обновляем статус
current_time = datetime.utcnow()
if panel_status == 'ACTIVE' and subscription.end_date > current_time:
new_status = SubscriptionStatus.ACTIVE.value
elif subscription.end_date <= current_time:
new_status = SubscriptionStatus.EXPIRED.value
elif panel_status == 'DISABLED':
new_status = SubscriptionStatus.DISABLED.value
else:
new_status = subscription.status # Оставляем текущий статус
if subscription.status != new_status:
subscription.status = new_status
logger.debug(f"Обновлен статус подписки: {new_status}")
# Обновляем использованный трафик
used_traffic_bytes = panel_user.get('usedTrafficBytes', 0)
traffic_used_gb = used_traffic_bytes / (1024**3)
if abs(subscription.traffic_used_gb - traffic_used_gb) > 0.01:
subscription.traffic_used_gb = traffic_used_gb
logger.debug(f"Обновлен использованный трафик: {traffic_used_gb} GB")
# Обновляем лимит трафика
traffic_limit_bytes = panel_user.get('trafficLimitBytes', 0)
traffic_limit_gb = traffic_limit_bytes // (1024**3) if traffic_limit_bytes > 0 else 0
if subscription.traffic_limit_gb != traffic_limit_gb:
subscription.traffic_limit_gb = traffic_limit_gb
logger.debug(f"Обновлен лимит трафика: {traffic_limit_gb} GB")
# Обновляем лимит устройств
device_limit = panel_user.get('hwidDeviceLimit', 1) or 1
if subscription.device_limit != device_limit:
subscription.device_limit = device_limit
logger.debug(f"Обновлен лимит устройств: {device_limit}")
# Обновляем RemnaWave UUID если отсутствует
if not subscription.remnawave_short_uuid:
subscription.remnawave_short_uuid = panel_user.get('shortUuid')
if not subscription.subscription_url:
subscription.subscription_url = panel_user.get('subscriptionUrl', '')
# Обновляем URL подписки если отсутствует или изменился
panel_url = panel_user.get('subscriptionUrl', '')
if not subscription.subscription_url or subscription.subscription_url != panel_url:
subscription.subscription_url = panel_url
# Обновляем подключенные сквады
active_squads = panel_user.get('activeInternalSquads', [])
squad_uuids = []
if isinstance(active_squads, list):
@@ -578,14 +674,20 @@ class RemnaWaveService:
elif isinstance(squad, str):
squad_uuids.append(squad)
if squad_uuids != subscription.connected_squads:
# Сравниваем сквады - обновляем только если есть изменения
current_squads = set(subscription.connected_squads or [])
new_squads = set(squad_uuids)
if current_squads != new_squads:
subscription.connected_squads = squad_uuids
logger.debug(f"Обновлены подключенные сквады: {squad_uuids}")
await db.commit()
logger.debug(f"✅ Обновлена подписка для пользователя {user.telegram_id}")
except Exception as e:
logger.error(f"❌ Ошибка обновления подписки для пользователя {user.telegram_id}: {e}")
await db.rollback()
async def sync_users_to_panel(self, db: AsyncSession) -> Dict[str, int]:
try:
@@ -865,4 +967,266 @@ class RemnaWaveService:
except Exception as e:
logger.error(f"Ошибка валидации данных пользователя: {e}")
return False
return False
async def cleanup_orphaned_subscriptions(self, db: AsyncSession) -> Dict[str, int]:
try:
stats = {"deactivated": 0, "errors": 0, "checked": 0}
logger.info("🧹 Начинаем очистку неактуальных подписок...")
async with self.api as api:
panel_users_data = await api._make_request('GET', '/api/users')
panel_users = panel_users_data['response']['users']
panel_telegram_ids = set()
for panel_user in panel_users:
telegram_id = panel_user.get('telegramId')
if telegram_id:
panel_telegram_ids.add(telegram_id)
logger.info(f"📊 Найдено {len(panel_telegram_ids)} пользователей в панели")
from app.database.crud.subscription import get_all_subscriptions
from app.database.models import SubscriptionStatus
page = 1
limit = 100
while True:
subscriptions, total_count = await get_all_subscriptions(db, page, limit)
if not subscriptions:
break
for subscription in subscriptions:
try:
stats["checked"] += 1
user = subscription.user
if subscription.status == SubscriptionStatus.DISABLED.value:
continue
if user.telegram_id not in panel_telegram_ids:
logger.info(f"🗑️ Деактивируем подписку пользователя {user.telegram_id} (отсутствует в панели)")
from app.database.crud.subscription import deactivate_subscription
await deactivate_subscription(db, subscription)
stats["deactivated"] += 1
except Exception as sub_error:
logger.error(f"❌ Ошибка обработки подписки {subscription.id}: {sub_error}")
stats["errors"] += 1
page += 1
if len(subscriptions) < limit:
break
logger.info(f"🧹 Очистка завершена: проверено {stats['checked']}, деактивировано {stats['deactivated']}, ошибок {stats['errors']}")
return stats
except Exception as e:
logger.error(f"❌ Критическая ошибка очистки подписок: {e}")
return {"deactivated": 0, "errors": 1, "checked": 0}
async def sync_subscription_statuses(self, db: AsyncSession) -> Dict[str, int]:
try:
stats = {"updated": 0, "errors": 0, "checked": 0}
logger.info("🔄 Начинаем синхронизацию статусов подписок...")
async with self.api as api:
panel_users_data = await api._make_request('GET', '/api/users')
panel_users = panel_users_data['response']['users']
panel_users_dict = {}
for panel_user in panel_users:
telegram_id = panel_user.get('telegramId')
if telegram_id:
panel_users_dict[telegram_id] = panel_user
logger.info(f"📊 Найдено {len(panel_users_dict)} пользователей в панели для синхронизации")
from app.database.crud.subscription import get_all_subscriptions
from app.database.models import SubscriptionStatus
from datetime import datetime
page = 1
limit = 100
while True:
subscriptions, total_count = await get_all_subscriptions(db, page, limit)
if not subscriptions:
break
for subscription in subscriptions:
try:
stats["checked"] += 1
user = subscription.user
panel_user = panel_users_dict.get(user.telegram_id)
if panel_user:
await self._update_subscription_from_panel_data(db, user, panel_user)
stats["updated"] += 1
else:
if subscription.status != SubscriptionStatus.DISABLED.value:
logger.info(f"🗑️ Деактивируем подписку пользователя {user.telegram_id} (нет в панели)")
from app.database.crud.subscription import deactivate_subscription
await deactivate_subscription(db, subscription)
stats["updated"] += 1
except Exception as sub_error:
logger.error(f"❌ Ошибка синхронизации подписки {subscription.id}: {sub_error}")
stats["errors"] += 1
page += 1
if len(subscriptions) < limit:
break
logger.info(f"🔄 Синхронизация статусов завершена: проверено {stats['checked']}, обновлено {stats['updated']}, ошибок {stats['errors']}")
return stats
except Exception as e:
logger.error(f"❌ Критическая ошибка синхронизации статусов: {e}")
return {"updated": 0, "errors": 1, "checked": 0}
async def validate_and_fix_subscriptions(self, db: AsyncSession) -> Dict[str, int]:
try:
stats = {"fixed": 0, "errors": 0, "checked": 0, "issues_found": 0}
logger.info("🔍 Начинаем валидацию подписок...")
from app.database.crud.subscription import get_all_subscriptions
from app.database.models import SubscriptionStatus
from datetime import datetime
page = 1
limit = 100
while True:
subscriptions, total_count = await get_all_subscriptions(db, page, limit)
if not subscriptions:
break
for subscription in subscriptions:
try:
stats["checked"] += 1
user = subscription.user
issues_fixed = 0
current_time = datetime.utcnow()
if subscription.end_date <= current_time and subscription.status == SubscriptionStatus.ACTIVE.value:
logger.info(f"🔧 Исправляем статус просроченной подписки {user.telegram_id}")
subscription.status = SubscriptionStatus.EXPIRED.value
issues_fixed += 1
if not subscription.remnawave_short_uuid and user.remnawave_uuid:
try:
async with self.api as api:
rw_user = await api.get_user_by_uuid(user.remnawave_uuid)
if rw_user:
subscription.remnawave_short_uuid = rw_user.short_uuid
subscription.subscription_url = rw_user.subscription_url
logger.info(f"🔧 Восстановлены данные RemnaWave для {user.telegram_id}")
issues_fixed += 1
except Exception as rw_error:
logger.warning(f"⚠️ Не удалось получить данные RemnaWave для {user.telegram_id}: {rw_error}")
if subscription.traffic_limit_gb < 0:
subscription.traffic_limit_gb = 0
logger.info(f"🔧 Исправлен некорректный лимит трафика для {user.telegram_id}")
issues_fixed += 1
if subscription.traffic_used_gb < 0:
subscription.traffic_used_gb = 0.0
logger.info(f"🔧 Исправлено некорректное использование трафика для {user.telegram_id}")
issues_fixed += 1
if subscription.device_limit <= 0:
subscription.device_limit = 1
logger.info(f"🔧 Исправлен лимит устройств для {user.telegram_id}")
issues_fixed += 1
if subscription.connected_squads is None:
subscription.connected_squads = []
logger.info(f"🔧 Инициализирован список сквадов для {user.telegram_id}")
issues_fixed += 1
if issues_fixed > 0:
stats["issues_found"] += issues_fixed
stats["fixed"] += 1
await db.commit()
except Exception as sub_error:
logger.error(f"❌ Ошибка валидации подписки {subscription.id}: {sub_error}")
stats["errors"] += 1
await db.rollback()
page += 1
if len(subscriptions) < limit:
break
logger.info(f"🔍 Валидация завершена: проверено {stats['checked']}, исправлено подписок {stats['fixed']}, найдено проблем {stats['issues_found']}, ошибок {stats['errors']}")
return stats
except Exception as e:
logger.error(f"❌ Критическая ошибка валидации: {e}")
return {"fixed": 0, "errors": 1, "checked": 0, "issues_found": 0}
async def get_sync_recommendations(self, db: AsyncSession) -> Dict[str, Any]:
try:
recommendations = {
"should_sync": False,
"sync_type": "none",
"reasons": [],
"priority": "low",
"estimated_time": "1-2 минуты"
}
from app.database.crud.user import get_users_list
bot_users = await get_users_list(db, offset=0, limit=10000)
users_without_uuid = sum(1 for user in bot_users if not user.remnawave_uuid and user.subscription)
from app.database.crud.subscription import get_expired_subscriptions
expired_subscriptions = await get_expired_subscriptions(db)
active_expired = sum(1 for sub in expired_subscriptions if sub.status == "active")
if users_without_uuid > 10:
recommendations["should_sync"] = True
recommendations["sync_type"] = "all"
recommendations["priority"] = "high"
recommendations["reasons"].append(f"Найдено {users_without_uuid} пользователей без связи с RemnaWave")
recommendations["estimated_time"] = "3-5 минут"
if active_expired > 5:
recommendations["should_sync"] = True
if recommendations["sync_type"] == "none":
recommendations["sync_type"] = "update_only"
recommendations["priority"] = "medium" if recommendations["priority"] == "low" else recommendations["priority"]
recommendations["reasons"].append(f"Найдено {active_expired} активных подписок с истекшим сроком")
if not recommendations["should_sync"]:
recommendations["sync_type"] = "update_only"
recommendations["reasons"].append("Рекомендуется регулярная синхронизация данных")
recommendations["estimated_time"] = "1-2 минуты"
return recommendations
except Exception as e:
logger.error(f"❌ Ошибка получения рекомендаций: {e}")
return {
"should_sync": True,
"sync_type": "all",
"reasons": ["Ошибка анализа - рекомендуется полная синхронизация"],
"priority": "medium",
"estimated_time": "3-5 минут"
}
+90 -11
View File
@@ -200,34 +200,113 @@ class SubscriptionService:
devices: int,
db: AsyncSession
) -> Tuple[int, List[int]]:
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
from app.database.crud.server_squad import get_server_squad_by_id
base_price = PERIOD_PRICES.get(period_days, 0)
traffic_price = TRAFFIC_PRICES.get(traffic_gb, 0)
server_prices = []
total_servers_price = 0
for server_id in server_squad_ids:
server = await get_server_squad_by_id(db, server_id)
if server and server.is_available and not server.is_full:
server_prices.append(server.price_kopeks)
total_servers_price += server.price_kopeks
logger.debug(f"🏷️ Сервер {server.display_name}: {server.price_kopeks/100}")
else:
server_prices.append(0)
logger.warning(f"⚠️ Сервер ID {server_id} недоступен")
devices_price = max(0, devices - 1) * settings.PRICE_PER_DEVICE
total_price = base_price + traffic_price + total_servers_price + devices_price
logger.info(f"💰 Расчет стоимости новой подписки:")
logger.info(f" 📅 Период {period_days} дней: {base_price/100}")
logger.info(f" 📊 Трафик {traffic_gb} ГБ: {traffic_price/100}")
logger.info(f" 🌍 Серверы ({len(server_squad_ids)}): {total_servers_price/100}")
logger.info(f" 📱 Устройства ({devices}): {devices_price/100}")
logger.info(f" 💎 ИТОГО: {total_price/100}")
return total_price, server_prices
async def _get_countries_price(self, country_uuids: List[str]) -> int:
# TODO: Реализовать получение цен из базы данных сквадов
# Пока возвращаем базовую логику
price_per_country = 1000
return len(country_uuids) * price_per_country
async def calculate_renewal_price(
self,
subscription: Subscription,
period_days: int,
db: AsyncSession
) -> int:
try:
from app.config import PERIOD_PRICES, TRAFFIC_PRICES
base_price = PERIOD_PRICES.get(period_days, 0)
servers_price, _ = await self.get_countries_price_by_uuids(
subscription.connected_squads, db
)
devices_price = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
traffic_price = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
total_price = base_price + servers_price + devices_price + traffic_price
logger.info(f"💰 Расчет стоимости продления для подписки {subscription.id} (по текущим ценам):")
logger.info(f" 📅 Период {period_days} дней: {base_price/100}")
logger.info(f" 🌍 Серверы ({len(subscription.connected_squads)}) по текущим ценам: {servers_price/100}")
logger.info(f" 📱 Устройства ({subscription.device_limit}): {devices_price/100}")
logger.info(f" 📊 Трафик ({subscription.traffic_limit_gb} ГБ): {traffic_price/100}")
logger.info(f" 💎 ИТОГО: {total_price/100}")
return total_price
except Exception as e:
logger.error(f"Ошибка расчета стоимости продления: {e}")
from app.config import PERIOD_PRICES
return PERIOD_PRICES.get(period_days, 0)
async def get_countries_price_by_uuids(
self,
country_uuids: List[str],
db: AsyncSession
) -> Tuple[int, List[int]]:
try:
from app.database.crud.server_squad import get_server_squad_by_uuid
total_price = 0
prices_list = []
for country_uuid in country_uuids:
server = await get_server_squad_by_uuid(db, country_uuid)
if server and server.is_available and not server.is_full:
price = server.price_kopeks
total_price += price
prices_list.append(price)
logger.debug(f"🏷️ Страна {server.display_name}: {price/100}")
else:
default_price = 1000
total_price += default_price
prices_list.append(default_price)
logger.warning(f"⚠️ Сервер {country_uuid} недоступен, используем базовую цену: {default_price/100}")
logger.info(f"💰 Общая стоимость стран: {total_price/100}")
return total_price, prices_list
except Exception as e:
logger.error(f"Ошибка получения цен стран: {e}")
default_prices = [1000] * len(country_uuids)
return sum(default_prices), default_prices
async def _get_countries_price(self, country_uuids: List[str], db: AsyncSession) -> int:
try:
total_price, _ = await self.get_countries_price_by_uuids(country_uuids, db)
return total_price
except Exception as e:
logger.error(f"Ошибка получения цен стран: {e}")
return len(country_uuids) * 1000
def _gb_to_bytes(self, gb: int) -> int:
if gb == 0:
@@ -237,4 +316,4 @@ class SubscriptionService:
def _bytes_to_gb(self, bytes_value: int) -> float:
if bytes_value == 0:
return 0.0
return bytes_value / (1024 * 1024 * 1024)
return bytes_value / (1024 * 1024 * 1024)
+130 -9
View File
@@ -2,6 +2,7 @@ import logging
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import delete
from app.database.crud.user import (
get_user_by_id, get_user_by_telegram_id, get_users_list,
@@ -10,7 +11,10 @@ from app.database.crud.user import (
)
from app.database.crud.transaction import get_user_transactions_count
from app.database.crud.subscription import get_subscription_by_user_id
from app.database.models import User, UserStatus
from app.database.models import (
User, UserStatus, Subscription, Transaction, PromoCodeUse,
ReferralEarning, SubscriptionServer
)
from app.config import settings
logger = logging.getLogger(__name__)
@@ -157,6 +161,19 @@ class UserService:
if not user:
return False
if user.remnawave_uuid:
try:
from app.services.subscription_service import SubscriptionService
subscription_service = SubscriptionService()
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован при блокировке")
except Exception as e:
logger.error(f"❌ Ошибка деактивации RemnaWave пользователя при блокировке: {e}")
if user.subscription:
from app.database.crud.subscription import deactivate_subscription
await deactivate_subscription(db, user.subscription)
await update_user(db, user, status=UserStatus.BLOCKED.value)
logger.info(f"Админ {admin_id} заблокировал пользователя {user_id}: {reason}")
@@ -179,6 +196,27 @@ class UserService:
await update_user(db, user, status=UserStatus.ACTIVE.value)
if user.subscription:
from datetime import datetime
from app.database.models import SubscriptionStatus
if user.subscription.end_date > datetime.utcnow():
user.subscription.status = SubscriptionStatus.ACTIVE.value
await db.commit()
await db.refresh(user.subscription)
logger.info(f"🔄 Подписка пользователя {user_id} восстановлена")
if user.remnawave_uuid:
try:
from app.services.subscription_service import SubscriptionService
subscription_service = SubscriptionService()
await subscription_service.update_remnawave_user(db, user.subscription)
logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} восстановлен при разблокировке")
except Exception as e:
logger.error(f"❌ Ошибка восстановления RemnaWave пользователя при разблокировке: {e}")
else:
logger.info(f"⏰ Подписка пользователя {user_id} истекла, восстановление невозможно")
logger.info(f"Админ {admin_id} разблокировал пользователя {user_id}")
return True
@@ -195,17 +233,100 @@ class UserService:
try:
user = await get_user_by_id(db, user_id)
if not user:
logger.warning(f"Пользователь {user_id} не найден для удаления")
return False
success = await delete_user(db, user)
logger.info(f"🗑️ Начинаем полное удаление пользователя {user_id} (Telegram ID: {user.telegram_id})")
if success:
logger.info(f"Админ {admin_id} удалил пользователя {user_id}")
if user.remnawave_uuid:
try:
from app.services.subscription_service import SubscriptionService
subscription_service = SubscriptionService()
await subscription_service.disable_remnawave_user(user.remnawave_uuid)
logger.info(f"✅ RemnaWave пользователь {user.remnawave_uuid} деактивирован")
except Exception as e:
logger.error(f"❌ Ошибка деактивации RemnaWave пользователя: {e}")
return success
if user.subscription:
try:
await db.execute(
delete(SubscriptionServer).where(
SubscriptionServer.subscription_id == user.subscription.id
)
)
logger.info(f"🗑️ Удалены записи SubscriptionServer для подписки {user.subscription.id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления SubscriptionServer: {e}")
if user.subscription:
try:
await db.delete(user.subscription)
logger.info(f"🗑️ Удалена подписка пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления подписки: {e}")
try:
await db.execute(
delete(PromoCodeUse).where(PromoCodeUse.user_id == user.id)
)
logger.info(f"🗑️ Удалены использования промокодов пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления использований промокодов: {e}")
try:
await db.execute(
delete(ReferralEarning).where(ReferralEarning.user_id == user.id)
)
logger.info(f"🗑️ Удалены реферальные доходы пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления реферальных доходов: {e}")
try:
await db.execute(
delete(ReferralEarning).where(ReferralEarning.referral_id == user.id)
)
logger.info(f"🗑️ Удалены реферальные записи о пользователе {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления реферальных записей: {e}")
try:
from sqlalchemy import update
await db.execute(
update(User)
.where(User.referred_by_id == user.id)
.values(referred_by_id=None)
)
logger.info(f"🗑️ Очищены реферальные ссылки у рефералов пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка очистки реферальных ссылок: {e}")
try:
await db.execute(
delete(Transaction).where(Transaction.user_id == user.id)
)
logger.info(f"🗑️ Удалены транзакции пользователя {user_id}")
except Exception as e:
logger.error(f"❌ Ошибка удаления транзакций: {e}")
try:
user.status = UserStatus.DELETED.value
user.balance_kopeks = 0
user.remnawave_uuid = None
user.updated_at = datetime.utcnow()
await db.commit()
logger.info(f"✅ Пользователь {user_id} помечен как удаленный и обнулен")
except Exception as e:
logger.error(f"❌ Ошибка обновления статуса пользователя: {e}")
await db.rollback()
return False
logger.info(f"✅ Пользователь {user_id} (Telegram ID: {user.telegram_id}) полностью удален админом {admin_id}")
return True
except Exception as e:
logger.error(f"Ошибка удаления пользователя: {e}")
logger.error(f"❌ Критическая ошибка удаления пользователя {user_id}: {e}")
await db.rollback()
return False
async def get_user_statistics(self, db: AsyncSession) -> Dict[str, Any]:
@@ -237,7 +358,7 @@ class UserService:
deleted_count = 0
for user in inactive_users:
success = await delete_user(db, user)
success = await self.delete_user_account(db, user.id, 0)
if success:
deleted_count += 1
@@ -281,7 +402,7 @@ class UserService:
"subscription_active": subscription.is_active if subscription else False,
"subscription_trial": subscription.is_trial if subscription else False,
"transactions_count": transactions_count,
"referrer_id": user.referrer_id,
"referrer_id": user.referred_by_id,
"referral_code": user.referral_code
}
@@ -330,4 +451,4 @@ class UserService:
except Exception as e:
logger.error(f"Ошибка получения пользователей по критериям: {e}")
return []
return []
+2 -2
View File
@@ -1,12 +1,12 @@
# Основные зависимости
aiogram==3.4.1
aiogram==3.7.0
aiohttp==3.9.1
asyncpg==0.29.0
SQLAlchemy==2.0.25
alembic==1.13.1
aiosqlite==0.19.0
# Дополнительные зависимости
# Дополнительные зависимости
pydantic==2.5.3
pydantic-settings==2.1.0
python-dotenv==1.0.0