Files
remnawave-bedolaga-telegram…/app/handlers/admin/main.py
2025-11-06 03:22:16 +03:00

453 lines
17 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
from aiogram import Dispatcher, types, F
from aiogram.filters import Command
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User
from app.keyboards.admin import (
get_admin_main_keyboard,
get_admin_users_submenu_keyboard,
get_admin_promo_submenu_keyboard,
get_admin_communications_submenu_keyboard,
get_admin_support_submenu_keyboard,
get_admin_settings_submenu_keyboard,
get_admin_system_submenu_keyboard
)
from app.localization.texts import get_texts
from app.handlers.admin import support_settings as support_settings_handlers
from app.utils.decorators import admin_required, error_handler
from app.services.support_settings_service import SupportSettingsService
from app.database.crud.rules import clear_all_rules, get_rules_statistics
from app.localization.texts import clear_rules_cache
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from app.database.crud.ticket import TicketCRUD
logger = logging.getLogger(__name__)
@admin_required
@error_handler
async def show_admin_panel(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
admin_text = texts.ADMIN_PANEL
try:
from app.services.remnawave_service import RemnaWaveService
remnawave_service = RemnaWaveService()
stats = await remnawave_service.get_system_statistics()
system_stats = stats.get("system", {})
users_online = system_stats.get("users_online", 0)
users_today = system_stats.get("users_last_day", 0)
users_week = system_stats.get("users_last_week", 0)
admin_text = admin_text.replace(
"\n\nВыберите раздел для управления:",
(
f"\n\n- 🟢 Онлайн сейчас: {users_online}"
f"\n- 📅 Онлайн сегодня: {users_today}"
f"\n- 🗓️ На этой неделе: {users_week}"
"\n\nВыберите раздел для управления:"
),
)
except Exception as e:
logger.error(f"Не удалось получить статистику Remnawave для админ-панели: {e}")
await callback.message.edit_text(
admin_text,
reply_markup=get_admin_main_keyboard(db_user.language)
)
await callback.answer()
@admin_required
@error_handler
async def show_users_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
texts.t("ADMIN_USERS_SUBMENU_TITLE", "👥 **Управление пользователями и подписками**\n\n") +
texts.t("ADMIN_SUBMENU_SELECT_SECTION", "Выберите нужный раздел:"),
reply_markup=get_admin_users_submenu_keyboard(db_user.language),
parse_mode="Markdown"
)
await callback.answer()
@admin_required
@error_handler
async def show_promo_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
texts.t("ADMIN_PROMO_SUBMENU_TITLE", "💰 **Промокоды и статистика**\n\n") +
texts.t("ADMIN_SUBMENU_SELECT_SECTION", "Выберите нужный раздел:"),
reply_markup=get_admin_promo_submenu_keyboard(db_user.language),
parse_mode="Markdown"
)
await callback.answer()
@admin_required
@error_handler
async def show_communications_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
texts.t("ADMIN_COMMUNICATIONS_SUBMENU_TITLE", "📨 **Коммуникации**\n\n") +
texts.t("ADMIN_COMMUNICATIONS_SUBMENU_DESCRIPTION", "Управление рассылками и текстами интерфейса:"),
reply_markup=get_admin_communications_submenu_keyboard(db_user.language),
parse_mode="Markdown"
)
await callback.answer()
@admin_required
@error_handler
async def show_support_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
# Moderators have access only to tickets and not to settings
is_moderator_only = (not settings.is_admin(callback.from_user.id) and SupportSettingsService.is_moderator(callback.from_user.id))
kb = get_admin_support_submenu_keyboard(db_user.language)
if is_moderator_only:
# Rebuild keyboard to include only tickets and back to main menu
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=texts.t("ADMIN_SUPPORT_TICKETS", "🎫 Тикеты поддержки"), callback_data="admin_tickets")],
[InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")]
])
await callback.message.edit_text(
texts.t("ADMIN_SUPPORT_SUBMENU_TITLE", "🛟 **Поддержка**\n\n") + (
texts.t("ADMIN_SUPPORT_SUBMENU_DESCRIPTION_MODERATOR", "Доступ к тикетам.")
if is_moderator_only
else texts.t("ADMIN_SUPPORT_SUBMENU_DESCRIPTION", "Управление тикетами и настройками поддержки:")
),
reply_markup=kb,
parse_mode="Markdown"
)
await callback.answer()
# Moderator panel entry (from main menu quick button)
async def show_moderator_panel(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=texts.t("ADMIN_SUPPORT_TICKETS", "🎫 Тикеты поддержки"), callback_data="admin_tickets")],
[InlineKeyboardButton(text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"), callback_data="back_to_menu")]
])
await callback.message.edit_text(
texts.t("ADMIN_SUPPORT_MODERATION_TITLE", "🧑‍⚖️ <b>Модерация поддержки</b>") + "\n\n" +
texts.t("ADMIN_SUPPORT_MODERATION_DESCRIPTION", "Доступ к тикетам поддержки."),
parse_mode="HTML",
reply_markup=kb
)
await callback.answer()
@admin_required
@error_handler
async def show_support_audit(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
# pagination
page = 1
if callback.data.startswith("admin_support_audit_page_"):
try:
page = int(callback.data.split("_")[-1])
except Exception:
page = 1
per_page = 10
total = await TicketCRUD.count_support_audit(db)
total_pages = max(1, (total + per_page - 1) // per_page)
if page < 1:
page = 1
if page > total_pages:
page = total_pages
offset = (page - 1) * per_page
logs = await TicketCRUD.list_support_audit(db, limit=per_page, offset=offset)
lines = [texts.t("ADMIN_SUPPORT_AUDIT_TITLE", "🧾 <b>Аудит модераторов</b>"), ""]
if not logs:
lines.append(texts.t("ADMIN_SUPPORT_AUDIT_EMPTY", "Пока пусто"))
else:
for log in logs:
role = (
texts.t("ADMIN_SUPPORT_AUDIT_ROLE_MODERATOR", "Модератор")
if getattr(log, 'is_moderator', False)
else texts.t("ADMIN_SUPPORT_AUDIT_ROLE_ADMIN", "Админ")
)
ts = log.created_at.strftime('%d.%m.%Y %H:%M') if getattr(log, 'created_at', None) else ''
action_map = {
'close_ticket': texts.t("ADMIN_SUPPORT_AUDIT_ACTION_CLOSE_TICKET", "Закрытие тикета"),
'block_user_timed': texts.t("ADMIN_SUPPORT_AUDIT_ACTION_BLOCK_TIMED", "Блокировка (время)"),
'block_user_perm': texts.t("ADMIN_SUPPORT_AUDIT_ACTION_BLOCK_PERM", "Блокировка (навсегда)"),
'close_all_tickets': texts.t("ADMIN_SUPPORT_AUDIT_ACTION_CLOSE_ALL_TICKETS", "Массовое закрытие тикетов"),
'unblock_user': texts.t("ADMIN_SUPPORT_AUDIT_ACTION_UNBLOCK", "Снятие блока"),
}
action_text = action_map.get(log.action, log.action)
ticket_part = f" тикет #{log.ticket_id}" if log.ticket_id else ""
details = log.details or {}
extra = ""
if log.action == 'block_user_timed' and 'minutes' in details:
extra = f" ({details['minutes']} мин)"
elif log.action == 'close_all_tickets' and 'count' in details:
extra = f" ({details['count']})"
lines.append(f"{ts}{role} <code>{log.actor_telegram_id}</code> — {action_text}{ticket_part}{extra}")
# keyboard with pagination
nav_row = []
if total_pages > 1:
if page > 1:
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"admin_support_audit_page_{page-1}"))
nav_row.append(InlineKeyboardButton(text=f"{page}/{total_pages}", callback_data="current_page"))
if page < total_pages:
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"admin_support_audit_page_{page+1}"))
kb_rows = []
if nav_row:
kb_rows.append(nav_row)
kb_rows.append([InlineKeyboardButton(text=texts.BACK, callback_data="admin_submenu_support")])
kb = InlineKeyboardMarkup(inline_keyboard=kb_rows)
await callback.message.edit_text("\n".join(lines), parse_mode="HTML", reply_markup=kb)
await callback.answer()
@admin_required
@error_handler
async def show_settings_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
texts.t("ADMIN_SETTINGS_SUBMENU_TITLE", "⚙️ **Настройки системы**\n\n") +
texts.t("ADMIN_SETTINGS_SUBMENU_DESCRIPTION", "Управление Remnawave, мониторингом и другими настройками:"),
reply_markup=get_admin_settings_submenu_keyboard(db_user.language),
parse_mode="Markdown"
)
await callback.answer()
@admin_required
@error_handler
async def show_system_submenu(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
texts = get_texts(db_user.language)
await callback.message.edit_text(
texts.t("ADMIN_SYSTEM_SUBMENU_TITLE", "🛠️ **Системные функции**\n\n") +
texts.t("ADMIN_SYSTEM_SUBMENU_DESCRIPTION", "Отчеты, обновления, логи, резервные копии и системные операции:"),
reply_markup=get_admin_system_submenu_keyboard(db_user.language),
parse_mode="Markdown"
)
await callback.answer()
@admin_required
@error_handler
async def clear_rules_command(
message: types.Message,
db_user: User,
db: AsyncSession
):
try:
stats = await get_rules_statistics(db)
if stats['total_active'] == 0:
await message.reply(
" <b>Правила уже очищены</b>\n\n"
"В системе нет активных правил. Используются стандартные правила по умолчанию."
)
return
success = await clear_all_rules(db, db_user.language)
if success:
clear_rules_cache()
await message.reply(
f"✅ <b>Правила успешно очищены!</b>\n\n"
f"📊 <b>Статистика:</b>\n"
f"• Очищено правил: {stats['total_active']}\n"
f"• Язык: {db_user.language}\n"
f"• Выполнил: {db_user.full_name}\n\n"
f"Теперь используются стандартные правила по умолчанию."
)
logger.info(f"Правила очищены командой администратором {db_user.telegram_id} ({db_user.full_name})")
else:
await message.reply(
"⚠️ <b>Нет правил для очистки</b>\n\n"
"Активные правила не найдены."
)
except Exception as e:
logger.error(f"Ошибка при очистке правил командой: {e}")
await message.reply(
"❌ <b>Ошибка при очистке правил</b>\n\n"
f"Произошла ошибка: {str(e)}\n"
"Попробуйте через админ-панель или повторите позже."
)
@admin_required
@error_handler
async def rules_stats_command(
message: types.Message,
db_user: User,
db: AsyncSession
):
try:
stats = await get_rules_statistics(db)
if 'error' in stats:
await message.reply(f"❌ Ошибка получения статистики: {stats['error']}")
return
text = f"📊 <b>Статистика правил сервиса</b>\n\n"
text += f"📋 <b>Общая информация:</b>\n"
text += f"• Активных правил: {stats['total_active']}\n"
text += f"Всего в истории: {stats['total_all_time']}\n"
text += f"• Поддерживаемых языков: {stats['total_languages']}\n\n"
if stats['languages']:
text += f"🌐 <b>По языкам:</b>\n"
for lang, lang_stats in stats['languages'].items():
text += f"• <code>{lang}</code>: {lang_stats['active_count']} правил, "
text += f"{lang_stats['content_length']} символов\n"
if lang_stats['last_updated']:
text += f" Обновлено: {lang_stats['last_updated'].strftime('%d.%m.%Y %H:%M')}\n"
else:
text += " Активных правил нет - используются правила по умолчанию"
await message.reply(text)
except Exception as e:
logger.error(f"Ошибка при получении статистики правил: {e}")
await message.reply(
f"❌ <b>Ошибка получения статистики</b>\n\n"
f"Произошла ошибка: {str(e)}"
)
@admin_required
@error_handler
async def admin_commands_help(
message: types.Message,
db_user: User,
db: AsyncSession
):
help_text = """
🔧 <b>Доступные админские команды:</b>
<b>📋 Управление правилами:</b>
• <code>/clear_rules</code> - очистить все правила
• <code>/rules_stats</code> - статистика правил
<b> Справка:</b>
• <code>/admin_help</code> - это сообщение
<b>📱 Панель управления:</b>
Используйте кнопку "Админ панель" в главном меню для полного доступа ко всем функциям.
<b>⚠️ Важно:</b>
Все команды логируются и требуют админских прав.
"""
await message.reply(help_text)
def register_handlers(dp: Dispatcher):
dp.callback_query.register(
show_admin_panel,
F.data == "admin_panel"
)
dp.callback_query.register(
show_users_submenu,
F.data == "admin_submenu_users"
)
dp.callback_query.register(
show_promo_submenu,
F.data == "admin_submenu_promo"
)
dp.callback_query.register(
show_communications_submenu,
F.data == "admin_submenu_communications"
)
dp.callback_query.register(
show_support_submenu,
F.data == "admin_submenu_support"
)
dp.callback_query.register(
show_support_audit,
F.data.in_(["admin_support_audit"]) | F.data.startswith("admin_support_audit_page_")
)
dp.callback_query.register(
show_settings_submenu,
F.data == "admin_submenu_settings"
)
dp.callback_query.register(
show_system_submenu,
F.data == "admin_submenu_system"
)
dp.callback_query.register(
show_moderator_panel,
F.data == "moderator_panel"
)
# Support settings module
support_settings_handlers.register_handlers(dp)
dp.message.register(
clear_rules_command,
Command("clear_rules")
)
dp.message.register(
rules_stats_command,
Command("rules_stats")
)
dp.message.register(
admin_commands_help,
Command("admin_help")
)