import logging
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.states import AdminStates
from app.database.models import User
from app.keyboards.admin import get_admin_subscriptions_keyboard
from app.localization.texts import get_texts
from app.database.crud.subscription import (
get_expiring_subscriptions, get_subscriptions_statistics, get_expired_subscriptions,
get_all_subscriptions
)
from app.services.subscription_service import SubscriptionService
from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_datetime, format_time_ago
def get_country_flag(country_name: str) -> str:
flags = {
'USA': '🇺🇸', 'United States': '🇺🇸', 'US': '🇺🇸',
'Germany': '🇩🇪', 'DE': '🇩🇪', 'Deutschland': '🇩🇪',
'Netherlands': '🇳🇱', 'NL': '🇳🇱', 'Holland': '🇳🇱',
'United Kingdom': '🇬🇧', 'UK': '🇬🇧', 'GB': '🇬🇧',
'Japan': '🇯🇵', 'JP': '🇯🇵',
'France': '🇫🇷', 'FR': '🇫🇷',
'Canada': '🇨🇦', 'CA': '🇨🇦',
'Russia': '🇷🇺', 'RU': '🇷🇺',
'Singapore': '🇸🇬', 'SG': '🇸🇬',
}
return flags.get(country_name, '🌍')
async def get_users_by_countries(db: AsyncSession) -> dict:
try:
result = await db.execute(
select(User.preferred_location, func.count(User.id))
.where(User.preferred_location.isnot(None))
.group_by(User.preferred_location)
)
stats = {}
for location, count in result.fetchall():
if location:
stats[location] = count
return stats
except Exception as e:
logger.error(f"Ошибка получения статистики по странам: {e}")
return {}
logger = logging.getLogger(__name__)
@admin_required
@error_handler
async def show_subscriptions_menu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
stats = await get_subscriptions_statistics(db)
text = f"""
📱 Управление подписками
📊 Статистика:
- Всего: {stats['total_subscriptions']}
- Активных: {stats['active_subscriptions']}
- Платных: {stats['paid_subscriptions']}
- Триальных: {stats['trial_subscriptions']}
📈 Продажи:
- Сегодня: {stats['purchased_today']}
- За неделю: {stats['purchased_week']}
- За месяц: {stats['purchased_month']}
Выберите действие:
"""
keyboard = [
[
types.InlineKeyboardButton(text="📋 Список подписок", callback_data="admin_subs_list"),
types.InlineKeyboardButton(text="⏰ Истекающие", callback_data="admin_subs_expiring")
],
[
types.InlineKeyboardButton(text="📊 Статистика", callback_data="admin_subs_stats"),
types.InlineKeyboardButton(text="🌍 География", callback_data="admin_subs_countries")
],
[
types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel")
]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_subscriptions_list(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession,
page: int = 1
):
subscriptions, total_count = await get_all_subscriptions(db, page=page, limit=10)
total_pages = (total_count + 9) // 10
if not subscriptions:
text = "📱 Список подписок\n\n❌ Подписки не найдены."
else:
text = f"📱 Список подписок\n\n"
text += f"📊 Всего: {total_count} | Страница: {page}/{total_pages}\n\n"
for i, sub in enumerate(subscriptions, 1 + (page - 1) * 10):
user_info = f"ID{sub.user.telegram_id}" if sub.user else "Неизвестно"
sub_type = "🎁" if sub.is_trial else "💎"
status = "✅ Активна" if sub.is_active else "❌ Неактивна"
text += f"{i}. {sub_type} {user_info}\n"
text += f" {status} | До: {format_datetime(sub.end_date)}\n"
if sub.device_limit > 0:
text += f" 📱 Устройств: {sub.device_limit}\n"
text += "\n"
keyboard = []
if total_pages > 1:
nav_row = []
if page > 1:
nav_row.append(types.InlineKeyboardButton(
text="⬅️", callback_data=f"admin_subs_list_page_{page-1}"
))
nav_row.append(types.InlineKeyboardButton(
text=f"{page}/{total_pages}", callback_data="current_page"
))
if page < total_pages:
nav_row.append(types.InlineKeyboardButton(
text="➡️", callback_data=f"admin_subs_list_page_{page+1}"
))
keyboard.append(nav_row)
keyboard.extend([
[types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_list")],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")]
])
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_expiring_subscriptions(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
expiring_3d = await get_expiring_subscriptions(db, 3)
expiring_1d = await get_expiring_subscriptions(db, 1)
expired = await get_expired_subscriptions(db)
text = f"""
⏰ Истекающие подписки
📊 Статистика:
- Истекают через 3 дня: {len(expiring_3d)}
- Истекают завтра: {len(expiring_1d)}
- Уже истекли: {len(expired)}
Истекают через 3 дня:
"""
for sub in expiring_3d[:5]:
user_info = f"ID{sub.user.telegram_id}" if sub.user else "Неизвестно"
sub_type = "🎁" if sub.is_trial else "💎"
text += f"{sub_type} {user_info} - {format_datetime(sub.end_date)}\n"
if len(expiring_3d) > 5:
text += f"... и еще {len(expiring_3d) - 5}\n"
text += f"\nИстекают завтра:\n"
for sub in expiring_1d[:5]:
user_info = f"ID{sub.user.telegram_id}" if sub.user else "Неизвестно"
sub_type = "🎁" if sub.is_trial else "💎"
text += f"{sub_type} {user_info} - {format_datetime(sub.end_date)}\n"
if len(expiring_1d) > 5:
text += f"... и еще {len(expiring_1d) - 5}\n"
keyboard = [
[types.InlineKeyboardButton(text="📨 Отправить напоминания", callback_data="admin_send_expiry_reminders")],
[types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_expiring")],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_subscriptions_stats(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
stats = await get_subscriptions_statistics(db)
expiring_3d = await get_expiring_subscriptions(db, 3)
expiring_7d = await get_expiring_subscriptions(db, 7)
expired = await get_expired_subscriptions(db)
text = f"""
📊 Детальная статистика подписок
📱 Общая информация:
• Всего подписок: {stats['total_subscriptions']}
• Активных: {stats['active_subscriptions']}
• Неактивных: {stats['total_subscriptions'] - stats['active_subscriptions']}
💎 По типам:
• Платных: {stats['paid_subscriptions']}
• Триальных: {stats['trial_subscriptions']}
📈 Продажи:
• Сегодня: {stats['purchased_today']}
• За неделю: {stats['purchased_week']}
• За месяц: {stats['purchased_month']}
⏰ Истечение:
• Истекают через 3 дня: {len(expiring_3d)}
• Истекают через 7 дней: {len(expiring_7d)}
• Уже истекли: {len(expired)}
💰 Конверсия:
• Из триала в платную: {stats.get('trial_to_paid_conversion', 0)}%
• Продлений: {stats.get('renewals_count', 0)}
"""
keyboard = [
# [
# types.InlineKeyboardButton(text="📊 Экспорт данных", callback_data="admin_subs_export"),
# types.InlineKeyboardButton(text="📈 Графики", callback_data="admin_subs_charts")
# ],
# [types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_stats")],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def show_countries_management(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
try:
from app.services.remnawave_service import RemnaWaveService
remnawave_service = RemnaWaveService()
nodes_data = await remnawave_service.get_all_nodes()
squads_data = await remnawave_service.get_all_squads()
text = "🌍 Управление странами\n\n"
if nodes_data:
text += "Доступные серверы:\n"
countries = {}
for node in nodes_data:
country_code = node.get('country_code', 'XX')
country_name = country_code
if country_name not in countries:
countries[country_name] = []
countries[country_name].append(node)
for country, nodes in countries.items():
active_nodes = len([n for n in nodes if n.get('is_connected') and n.get('is_node_online')])
total_nodes = len(nodes)
country_flag = get_country_flag(country)
text += f"{country_flag} {country}: {active_nodes}/{total_nodes} серверов\n"
total_users_online = sum(n.get('users_online', 0) or 0 for n in nodes)
if total_users_online > 0:
text += f" 👥 Пользователей онлайн: {total_users_online}\n"
else:
text += "❌ Не удалось загрузить данные о серверах\n"
if squads_data:
text += f"\nВсего сквадов: {len(squads_data)}\n"
total_members = sum(squad.get('members_count', 0) for squad in squads_data)
text += f"Участников в сквадах: {total_members}\n"
text += "\nСквады:\n"
for squad in squads_data[:5]:
name = squad.get('name', 'Неизвестно')
members = squad.get('members_count', 0)
inbounds = squad.get('inbounds_count', 0)
text += f"• {name}: {members} участников, {inbounds} inbound(s)\n"
if len(squads_data) > 5:
text += f"... и еще {len(squads_data) - 5} сквадов\n"
user_stats = await get_users_by_countries(db)
if user_stats:
text += "\nПользователи по регионам:\n"
for country, count in user_stats.items():
country_flag = get_country_flag(country)
text += f"{country_flag} {country}: {count} пользователей\n"
except Exception as e:
logger.error(f"Ошибка получения данных о странах: {e}")
text = f"""
🌍 Управление странами
❌ Ошибка загрузки данных
Не удалось получить информацию о серверах.
Проверьте подключение к RemnaWave API.
Детали ошибки: {str(e)}
"""
keyboard = [
[
types.InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_subs_countries")
],
[
types.InlineKeyboardButton(text="📊 Статистика нод", callback_data="admin_rw_nodes"),
types.InlineKeyboardButton(text="🔧 Сквады", callback_data="admin_rw_squads")
],
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")]
]
await callback.message.edit_text(
text,
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
)
await callback.answer()
@admin_required
@error_handler
async def send_expiry_reminders(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
await callback.message.edit_text(
"📨 Отправка напоминаний...\n\nПодождите, это может занять время.",
reply_markup=None
)
expiring_subs = await get_expiring_subscriptions(db, 1)
sent_count = 0
for subscription in expiring_subs:
if subscription.user:
try:
user = subscription.user
days_left = max(1, subscription.days_left)
reminder_text = f"""
⚠️ Подписка истекает!
Ваша подписка истекает через {days_left} день(а).
Не забудьте продлить подписку, чтобы не потерять доступ к серверам.
💎 Продлить подписку можно в главном меню.
"""
await callback.bot.send_message(
chat_id=user.telegram_id,
text=reminder_text
)
sent_count += 1
except Exception as e:
logger.error(f"Ошибка отправки напоминания пользователю {subscription.user_id}: {e}")
await callback.message.edit_text(
f"✅ Напоминания отправлены: {sent_count} из {len(expiring_subs)}",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subs_expiring")]
])
)
await callback.answer()
@admin_required
@error_handler
async def handle_subscriptions_pagination(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
page = int(callback.data.split('_')[-1])
await show_subscriptions_list(callback, db_user, db, page)
def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_subscriptions_menu, F.data == "admin_subscriptions")
dp.callback_query.register(show_subscriptions_list, F.data == "admin_subs_list")
dp.callback_query.register(show_expiring_subscriptions, F.data == "admin_subs_expiring")
dp.callback_query.register(show_subscriptions_stats, F.data == "admin_subs_stats")
dp.callback_query.register(show_countries_management, F.data == "admin_subs_countries")
dp.callback_query.register(send_expiry_reminders, F.data == "admin_send_expiry_reminders")
dp.callback_query.register(
handle_subscriptions_pagination,
F.data.startswith("admin_subs_list_page_")
)