""" Админские команды Только для владельца и доверенных лиц """ import logging from aiogram import Router, F from aiogram.types import Message, CallbackQuery from aiogram.filters import Command, StateFilter from aiogram.fsm.state import State, StatesGroup from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from database.db import AsyncSessionLocal from database.models import User from utils.decorators import admin_only, admin_callback_only from utils.formatters import format_stats, format_user_info, get_spy_status from services.analytics import ChatAnalytics from services.spy_detector import SpyDetector from keyboards.inline import get_admin_menu, get_spy_detail_menu from keyboards.admin_menu import get_admin_main_menu, get_admin_reply_menu, get_admin_help_menu, get_admin_rating_menu import config logger = logging.getLogger(__name__) router = Router() # ============================================================================= # FSM для рассылки # ============================================================================= class BroadcastState(StatesGroup): waiting_for_message = State() # ============================================================================= # АДМИН МЕНЮ # ============================================================================= @router.message(Command('admin')) @admin_only async def cmd_admin(message: Message): """Админ-меню (только в личке!)""" # Проверяем что это личка if message.chat.type != 'private': await message.answer( '⚠️ Админ-панель доступна только в личных сообщениях!\n\n' 'Напишите боту в личку: /admin', parse_mode='HTML' ) return await message.answer( '🛡️ Панель администратора\n\n' 'Выберите раздел:', reply_markup=get_admin_main_menu(), parse_mode='HTML' ) @router.message(F.text == '📊 Статистика') @admin_only async def msg_stats_button(message: Message): """Кнопка Статистика""" async with AsyncSessionLocal() as session: analytics = ChatAnalytics(session) stats = await analytics.get_general_stats() text = format_stats(stats) await message.answer(text, parse_mode='HTML') @router.message(F.text == '🕵️ Шпионы') @admin_only async def msg_spies_button(message: Message): """Кнопка Шпионы""" async with AsyncSessionLocal() as session: spy_detector = SpyDetector(session) spies = await spy_detector.get_spies() if not spies: await message.answer('🟢 Шпионов не обнаружено') return text = '🚨 ШПИОНЫ 🚨\n\n' for spy in spies: name = spy.full_name or f'user{spy.user_id}' apt = spy.apartment or '?' text += f'🔴 {name} (кв. {apt}) — Score: {spy.spy_score}\n' await message.answer(text, parse_mode='HTML') @router.message(F.text == '👥 Пользователи') @admin_only async def msg_users_button(message: Message): """Кнопка Пользователи""" async with AsyncSessionLocal() as session: stmt = select(User).limit(50) result = await session.execute(stmt) users = list(result.scalars().all()) text = f'👥 Пользователи (50):\n\n' for user in users: status = '✅' if user.verified else '⏳' name = user.full_name or f'user{user.user_id}' apt = user.apartment or '?' phone = user.phone or '📭' text += f'{status} {user.user_id} — {name}\n' text += f' └─ кв. {apt} | 📱 {phone}\n' await message.answer(text, parse_mode='HTML') @router.message(F.text == '❓ Помощь') @admin_only async def msg_help_button(message: Message): """Кнопка Помощь - справка по командам""" help_text = ''' 🛡️ СПРАВКА АДМИНИСТРАТОРА 📋 Основные команды: 📊 Статистика и пользователи: • /stats — Статистика чата • /users — Список всех пользователей • /unverified_list — С отозванной верификацией 🏆 Рейтинг и активность: • /rating — Рейтинг пользователя • /top — Топ-10 по рейтингу • /rank — Распределение по уровням 🕵️ Безопасность: • /spies — Список шпионов (score > 60) • /ban — Забанить пользователя • /unban — Разбанить • /revoke — Снять верификацию • /unverify — Отозвать (7 дней) 🏠 Квартиры: • /apt <номер> — Жильцы квартиры • /apt_stats — Статистика по квартирам • /find <часть> — Поиск квартиры ⚠️ Предупреждения (анти-мат): • /warnings — Список предупреждений • /clear_warnings — Сбросить • /add_bad_word — Добавить слово • /remove_bad_word — Удалить слово 📤 Рассылка и экспорт: • /admin → кнопка «Рассылка» • /export — Экспорт JSON • /check_unverified — Кто не верифицирован >7 дней ℹ️ Общее: • /admin — Админ-меню • /admin_help — Эта справка • /cancel — Отменить действие 💡 Совет: Используйте кнопки в меню для быстрого доступа! ''' await message.answer(help_text, parse_mode='HTML') @router.callback_query(F.data == 'admin_stats') @admin_callback_only async def cb_admin_stats(callback: CallbackQuery): """Статистика чата""" await callback.answer() async with AsyncSessionLocal() as session: analytics = ChatAnalytics(session) stats = await analytics.get_general_stats() text = format_stats(stats) await callback.message.answer(text, parse_mode='HTML') @router.callback_query(F.data == 'admin_spies') @admin_callback_only async def cb_admin_spies(callback: CallbackQuery): """Список подозрительных""" await callback.answer() async with AsyncSessionLocal() as session: spy_detector = SpyDetector(session) suspicious = await spy_detector.get_suspicious_users(min_score=30) if not suspicious: await callback.message.answer('🟢 Подозрительных пользователей нет') return text = f'🕵️ Подозрительные пользователи ({len(suspicious)}):\n\n' for user in suspicious: status_emoji = '🔴' if user.spy_score > 60 else '🟡' name = user.full_name or f'user{user.user_id}' apt = user.apartment or '?' text += f'{status_emoji} {name} (кв. {apt}) — Score: {user.spy_score}\n' text += '\nНажмите на пользователя для деталей:' await callback.message.answer(text, parse_mode='HTML') @router.callback_query(F.data == 'admin_users') @admin_callback_only async def cb_admin_users(callback: CallbackQuery): """Список всех пользователей""" await callback.answer() async with AsyncSessionLocal() as session: stmt = select(User).order_by(User.join_date.desc()).limit(50) result = await session.execute(stmt) users = list(result.scalars().all()) # Считаем сколько человек в каждой квартире apt_count_stmt = select(User.apartment, func.count(User.user_id).label('count')).group_by(User.apartment) apt_count_result = await session.execute(apt_count_stmt) apt_counts = {row.apartment: row.count for row in apt_count_result.all()} text = f'👥 Пользователи (последние 50):\n\n' for user in users: status = '✅' if user.verified else '⏳' name = user.full_name or f'user{user.user_id}' username = f'@{user.username}' if user.username else '' apt = user.apartment or '?' # Показываем сколько человек в квартире apt_info = '' if user.apartment and apt_counts.get(user.apartment, 1) > 1: apt_info = f' 🏠({apt_counts[user.apartment]})' # Показываем ID обязательно! text += f'{status} {user.user_id} — {name} {username}{apt_info}\n' text += f' └─ кв. {apt} | сообщ: {user.message_count}\n' await callback.message.answer(text, parse_mode='HTML') @router.callback_query(F.data == 'admin_unverified') @admin_callback_only async def cb_admin_unverified(callback: CallbackQuery): """Непроверенные пользователи""" await callback.answer() async with AsyncSessionLocal() as session: stmt = ( select(User) .where(User.verified == False) .order_by(User.join_date) .limit(50) ) result = await session.execute(stmt) users = list(result.scalars().all()) if not users: await callback.message.answer('✅ Все пользователи верифицированы') return text = f'⏳ Непроверенные ({len(users)}):\n\n' for user in users: name = user.full_name or f'user{user.user_id}' apt = user.apartment or '?' join_date = user.join_date.strftime('%d.%m.%Y') if user.join_date else '?' text += f'• {name} (кв. {apt}) — с {join_date}\n' await callback.message.answer(text, parse_mode='HTML') @router.callback_query(F.data == 'admin_export') @admin_callback_only async def cb_admin_export(callback: CallbackQuery): """Принудительный экспорт""" await callback.answer('🔄 Экспорт начался...') async with AsyncSessionLocal() as session: analytics = ChatAnalytics(session) filepath = await analytics.export_to_json() await callback.message.answer( f'✅ Экспорт завершён\n\n' f'Файл: {filepath}\n' f'Проверьте папку exports/' ) @router.callback_query(F.data == 'admin_broadcast') @admin_callback_only async def cb_admin_broadcast(callback: CallbackQuery): """Рассылка сообщений""" await callback.answer() # Проверяем что это личка if callback.message.chat.type != 'private': await callback.message.answer( '⚠️ Рассылка доступна только в личных сообщениях!\n\n' 'Напишите боту в личку и выберите эту команду там.', parse_mode='HTML' ) return await callback.message.answer( '📤 Рассылка\n\n' 'Теперь отправьте сообщение, которое нужно разослать всем жильцам.\n\n' 'Или отмените командой /cancel', parse_mode='HTML' ) # Включаем состояние await callback.bot.set_state(callback.from_user.id, BroadcastState.waiting_for_message) # ============================================================================= # ОБРАБОТКА РАССЫЛКИ # ============================================================================= @router.message(StateFilter(BroadcastState.waiting_for_message)) @admin_only async def process_broadcast_message(message: Message): """Обработка сообщения для рассылки""" broadcast_text = message.text # Отключаем состояние await message.bot.delete_state(message.from_user.id) # Подтверждаем await message.answer( f'📤 Начинаю рассылку...\n\n' f'Сообщение: {broadcast_text[:100]}...' ) # Отправляем в чат дома try: await message.bot.send_message( config.ADMIN_CHAT_ID, f'📢 Объявление от администрации\n\n' f'{broadcast_text}', parse_mode='HTML' ) logger.info(f'Рассылка отправлена в чат {config.ADMIN_CHAT_ID}') except Exception as e: logger.error(f'Ошибка отправки в чат: {e}') # Рассылаем всем верифицированным в личку async with AsyncSessionLocal() as session: stmt = select(User).where(User.verified == True) result = await session.execute(stmt) users = list(result.scalars().all()) success_count = 0 error_count = 0 for user in users: try: await message.bot.send_message( user.user_id, f'📢 Объявление от администрации\n\n' f'{broadcast_text}', parse_mode='HTML' ) success_count += 1 except Exception as e: logger.error(f'Не удалось отправить пользователю {user.user_id}: {e}') error_count += 1 # Отчёт админу await message.answer( f'✅ Рассылка завершена!\n\n' f'Успешно: {success_count}\n' f'Ошибок: {error_count}\n' f'В чат: ✅', parse_mode='HTML' ) # ============================================================================= # СНЯТИЕ ВЕРИФИКАЦИИ (БЫСТРОЕ) # ============================================================================= @router.message(Command('revoke')) @admin_only async def cmd_revoke(message: Message): """ Снять верификацию с пользователя Использование: /revoke """ args = message.text.split() if len(args) < 2: await message.answer( 'Использование: /revoke <user_id>\n\n' 'Пример: /revoke 123456789\n\n' 'Пользователь должен будет пройти верификацию заново.', parse_mode='HTML' ) return try: user_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID пользователя') return async with AsyncSessionLocal() as session: # Находим пользователя stmt = select(User).where(User.user_id == user_id) result = await session.execute(stmt) user = result.scalar_one_or_none() if not user: await message.answer('❌ Пользователь не найден') return # Проверяем, не админ ли это if user.is_admin: await message.answer('❌ Нельзя снять верификацию с администратора!') return # Если уже не верифицирован if not user.verified: await message.answer(f'ℹ️ Пользователь {user.full_name} уже не верифицирован') return # Снимаем верификацию user.verified = False # Создаём запись в логе from database.models import VerificationRequest from datetime import datetime verification_log = VerificationRequest( user_id=user_id, apartment=user.apartment, status='revoked', comment='Снято верификацию администратором', reviewed_at=datetime.utcnow(), reviewed_by=message.from_user.id, ) session.add(verification_log) await session.commit() logger.info(f'Верификация снята с user_id={user_id}') # Уведомляем пользователя (в личку) try: await message.bot.send_message( user_id, f'⚠️ Верификация снята\n\n' f'Администратор снял вашу верификацию.\n\n' f'❗ Что дальше:\n' f'Напишите боту номер квартиры ещё раз для повторной верификации.', parse_mode='HTML' ) except Exception as e: logger.error(f'Не удалось уведомить пользователя: {e}') # Отвечаем админу await message.answer( f'✅ Верификация снята\n\n' f'Пользователь: {user.full_name}\n' f'Квартира: {user.apartment}\n' f'ID: {user.user_id}\n\n' f'Теперь пользователь должен пройти верификацию заново.', parse_mode='HTML' ) # ============================================================================= # ОТМЕНА # ============================================================================= @router.message(Command('cancel')) @admin_only async def cmd_cancel(message: Message): """Отмена текущего действия""" await message.bot.delete_state(message.from_user.id) await message.answer('❌ Отменено') # ============================================================================= # БЫСТРЫЕ КОМАНДЫ # ============================================================================= @router.message(Command('stats')) @router.message(Command('статистика')) @admin_only async def cmd_stats(message: Message): """Быстрая статистика""" async with AsyncSessionLocal() as session: analytics = ChatAnalytics(session) stats = await analytics.get_general_stats() text = format_stats(stats) await message.answer(text, parse_mode='HTML') @router.message(Command('spies')) @router.message(Command('шпионы')) @admin_only async def cmd_spies(message: Message): """Список шпионов""" async with AsyncSessionLocal() as session: spy_detector = SpyDetector(session) spies = await spy_detector.get_spies() if not spies: await message.answer('🟢 Шпионов не обнаружено') return text = '🚨 ШПИОНЫ 🚨\n\n' for spy in spies: name = spy.full_name or f'user{spy.user_id}' apt = spy.apartment or '?' text += f'🔴 {name} (кв. {apt}) — Score: {spy.spy_score}\n' if spy.spy_flags: import json flags = json.loads(spy.spy_flags) text += f' Флаги: {", ".join(flags)}\n' await message.answer(text, parse_mode='HTML') @router.message(Command('export')) @router.message(Command('экспорт')) @admin_only async def cmd_export(message: Message): """Экспорт данных""" async with AsyncSessionLocal() as session: analytics = ChatAnalytics(session) filepath = await analytics.export_to_json() await message.answer(f'✅ Экспорт: {filepath}') @router.message(Command('users')) @router.message(Command('пользователи')) @admin_only async def cmd_users(message: Message): """Список пользователей""" async with AsyncSessionLocal() as session: stmt = select(User).limit(100) result = await session.execute(stmt) users = list(result.scalars().all()) text = f'👥 Пользователи ({len(users)}):\n\n' for user in users[:50]: status = '✅' if user.verified else '⏳' name = user.full_name or f'user{user.user_id}' apt = user.apartment or '?' text += f'{status} {name} (кв. {apt})\n' if len(users) > 50: text += f'\n... и ещё {len(users) - 50}' await message.answer(text, parse_mode='HTML') @router.message(Command('admin_help')) @router.message(Command('помощь')) @admin_only async def cmd_admin_help(message: Message): """Справка администратора""" help_text = ''' 🛡️ СПРАВКА АДМИНИСТРАТОРА 📋 Основные команды: 📊 Статистика и пользователи: • /stats — Статистика чата • /users — Список всех пользователей • /unverified_list — С отозванной верификацией 🏆 Рейтинг и активность: • /rating — Рейтинг пользователя • /top — Топ-10 по рейтингу • /rank — Распределение по уровням 🕵️ Безопасность: • /spies — Список шпионов (score > 60) • /ban — Забанить пользователя • /unban — Разбанить • /revoke — Снять верификацию • /unverify — Отозвать (7 дней) 🏠 Квартиры: • /apt <номер> — Жильцы квартиры • /apt_stats — Статистика по квартирам • /find <часть> — Поиск квартиры 📱 Телефоны: • /add_phone <тел> — Добавить телефон • /edit_phone <тел> — Изменить телефон • /remove_phone — Удалить телефон • /phone — Показать телефон ⚠️ Предупреждения (анти-мат): • /warnings — Список предупреждений • /clear_warnings — Сбросить • /add_bad_word — Добавить слово • /remove_bad_word — Удалить слово 📤 Рассылка и экспорт: • /admin → кнопка «Рассылка» • /export — Экспорт JSON • /check_unverified — Кто не верифицирован >7 дней ℹ️ Общее: • /admin — Админ-меню • /admin_help — Эта справка • /cancel — Отменить действие 💡 Совет: Используйте кнопки в меню для быстрого доступа! ''' await message.answer(help_text, parse_mode='HTML') # ============================================================================= # РЕЙТИНГ ПОЛЬЗОВАТЕЛЕЙ # ============================================================================= @router.message(Command('rating')) @router.message(Command('рейтинг')) @admin_only async def cmd_rating(message: Message): """ Показать рейтинг конкретного пользователя Использование: /rating или /rating <@username> """ # Проверяем что это личка if message.chat.type != 'private': await message.answer( '⚠️ Команда доступна только в личных сообщениях!\n\n' 'Напишите боту в личку: /rating', parse_mode='HTML' ) return args = message.text.split()[1:] if message.text else [] if not args: # Показать свой рейтинг user_id = message.from_user.id async with AsyncSessionLocal() as session: stmt = select(User).where(User.user_id == user_id) result = await session.execute(stmt) user = result.scalar_one_or_none() if user: text = format_rating_user(user) await message.answer(text, parse_mode='HTML') else: await message.answer('❌ Пользователь не найден') return # Поиск по ID или username target = args[0] async with AsyncSessionLocal() as session: if target.isdigit(): # Поиск по ID user_id = int(target) stmt = select(User).where(User.user_id == user_id) else: # Поиск по username username = target.lstrip('@') stmt = select(User).where(User.username == username) result = await session.execute(stmt) user = result.scalar_one_or_none() if not user: await message.answer(f'❌ Пользователь не найден: {target}') return text = format_rating_user(user) await message.answer(text, parse_mode='HTML') @router.message(Command('top')) @router.message(Command('топ')) @admin_only async def cmd_top(message: Message): """ Топ пользователей по рейтингу """ # Проверяем что это личка if message.chat.type != 'private': await message.answer( '⚠️ Команда доступна только в личных сообщениях!\n\n' 'Напишите боту в личку: /top', parse_mode='HTML' ) return async with AsyncSessionLocal() as session: # Топ-10 по рейтингу stmt = select(User).where(User.rating > 0).order_by(User.rating.desc()).limit(10) result = await session.execute(stmt) users = list(result.scalars().all()) if not users: await message.answer('📊 Топ пользователей пуст') return text = '🏆 ТОП-10 ПО РЕЙТИНГУ\n\n' for i, user in enumerate(users, 1): medal = '🥇' if i == 1 else '🥈' if i == 2 else '🥉' if i == 3 else f'{i}.' name = user.full_name or f'user{user.user_id}' apt = f' (кв. {user.apartment})' if user.apartment else '' text += f'{medal} {name}{apt}\n' text += f' └─ Рейтинг: {user.rating} | Уровень: {user.level}\n' await message.answer(text, parse_mode='HTML') @router.message(Command('rank')) @router.message(Command('ранг')) @admin_only async def cmd_rank(message: Message): """ Показать распределение по уровням """ # Проверяем что это личка if message.chat.type != 'private': await message.answer( '⚠️ Команда доступна только в личных сообщениях!\n\n' 'Напишите боту в личку: /rank', parse_mode='HTML' ) return async with AsyncSessionLocal() as session: # Считаем по уровням stmt = select(User.level, func.count(User.user_id)).group_by(User.level) result = await session.execute(stmt) levels = dict(result.all()) # Всего пользователей total_stmt = select(func.count(User.user_id)) total = await session.scalar(total_stmt) text = '📊 РАСПРЕДЕЛЕНИЕ ПО УРОВНЯМ\n\n' text += f'Всего пользователей: {total}\n\n' # Уровни по порядку level_order = ['Лидер', 'Активист', 'Сосед', 'Новичок'] level_icons = { 'Лидер': '👑', 'Активист': '⭐', 'Сосед': '🏠', 'Новичок': '🌱' } for level in level_order: count = levels.get(level, 0) icon = level_icons.get(level, '•') percent = round(count / total * 100, 1) if total > 0 else 0 text += f'{icon} {level}: {count} ({percent}%)\n' text += '\nУровни:\n' text += '🌱 Новичок: 0-99 баллов\n' text += '🏠 Сосед: 100-499 баллов\n' text += '⭐ Активист: 500-999 баллов\n' text += '👑 Лидер: 1000+ баллов' await message.answer(text, parse_mode='HTML') def format_rating_user(user: User) -> str: """Форматировать информацию о рейтинге пользователя""" name = user.full_name or f'user{user.user_id}' apt = f' (кв. {user.apartment})' if user.apartment else '' # Иконка уровня level_icons = { 'Лидер': '👑', 'Активист': '⭐', 'Сосед': '🏠', 'Новичок': '🌱' } level_icon = level_icons.get(user.level, '•') text = f'📊 РЕЙТИНГ ПОЛЬЗОВАТЕЛЯ\n\n' text += f'👤 {name}{apt}\n' text += f'ID: {user.user_id}\n\n' text += f'{level_icon} Уровень: {user.level}\n' text += f'🏆 Рейтинг: {user.rating} баллов\n' text += f'💬 Сообщений: {user.message_count}\n' # До следующего уровня next_levels = { 'Новичок': 100, 'Сосед': 500, 'Активист': 1000, 'Лидер': None } next_level = next_levels.get(user.level) if next_level: to_next = next_level - user.rating text += f'📈 До следующего уровня: {to_next} баллов\n' return text # ============================================================================= # ОБРАБОТЧИКИ КНОПОК РЕЙТИНГА # ============================================================================= @router.callback_query(F.data == 'admin_rating') @admin_only async def cb_admin_rating(message: CallbackQuery): """Кнопка Рейтинг - показываем меню рейтинга""" # Показываем топ-10 сразу async with AsyncSessionLocal() as session: stmt = select(User).where(User.rating > 0).order_by(User.rating.desc()).limit(10) result = await session.execute(stmt) users = list(result.scalars().all()) if not users: await message.answer('📊 Топ пользователей пуст') return text = '🏆 ТОП-10 ПО РЕЙТИНГУ\n\n' for i, user in enumerate(users, 1): medal = '🥇' if i == 1 else '🥈' if i == 2 else '🥉' if i == 3 else f'{i}.' name = user.full_name or f'user{user.user_id}' apt = f' (кв. {user.apartment})' if user.apartment else '' text += f'{medal} {name}{apt}\n' text += f' └─ Рейтинг: {user.rating} | Уровень: {user.level}\n' await message.answer(text, reply_markup=get_admin_rating_menu(), parse_mode='HTML') @router.callback_query(F.data == 'admin_top') @admin_only async def cb_admin_top(message: CallbackQuery): """Кнопка Топ-10""" async with AsyncSessionLocal() as session: stmt = select(User).where(User.rating > 0).order_by(User.rating.desc()).limit(10) result = await session.execute(stmt) users = list(result.scalars().all()) if not users: await message.answer('📊 Топ пользователей пуст') return text = '🏆 ТОП-10 ПО РЕЙТИНГУ\n\n' for i, user in enumerate(users, 1): medal = '🥇' if i == 1 else '🥈' if i == 2 else '🥉' if i == 3 else f'{i}.' name = user.full_name or f'user{user.user_id}' apt = f' (кв. {user.apartment})' if user.apartment else '' text += f'{medal} {name}{apt}\n' text += f' └─ Рейтинг: {user.rating} | Уровень: {user.level}\n' await message.answer(text, reply_markup=get_admin_rating_menu(), parse_mode='HTML') @router.callback_query(F.data == 'admin_help') @admin_only async def cb_admin_help(message: CallbackQuery): """Кнопка Помощь""" text = ( '❓ Помощь администратору\n\n' 'Основные команды:\n' '• /admin — Админ-панель\n' '• /stats — Статистика чата\n' '• /spies — Подозрительные пользователи\n' '• /users — Список жильцов\n' '• /verify — Заявки на верификацию\n' '• /export — Экспорт данных\n' '• /rating — Рейтинг пользователей\n' '\n' 'Инициативная группа:\n' '• /ig_add — Добавить в ИГ\n' '• /ig_remove — Удалить из ИГ\n' '• /ig_list — Список ИГ\n' '• /ig_broadcast — Рассылка для ИГ\n' '\n' 'События:\n' '• /event_add — Добавить событие\n' '• /events — Список событий\n' '• /participants — Участники\n' '\n' '💡 Все команды доступны через меню!' ) await message.message.answer(text, parse_mode='HTML') await message.answer() @router.callback_query(F.data == 'admin_unverify') @admin_only async def cb_admin_unverify(message: CallbackQuery): """Кнопка Отозвать верификацию""" text = ( '🚫 Отозвать верификацию\n\n' 'Использование:\n' '/unverify <user_id> [причина]\n\n' 'Пример:\n' '/unverify 123456789 Подозрение на шпиона\n\n' '💡 User ID можно узнать в /users' ) await message.message.answer(text, parse_mode='HTML') await message.answer() @router.callback_query(F.data == 'admin_unverified_list') @admin_only async def cb_admin_unverified_list(message: CallbackQuery): """Кнопка Список не верифицированных""" await message.message.answer( '📋 Список не верифицированных\n\n' 'Использование:\n' '/unverified_list\n\n' 'Показывает кто был лишён верификации' ) await message.answer() @router.callback_query(F.data == 'admin_rank') @admin_only async def cb_admin_rank(message: CallbackQuery): """Кнопка Уровни""" async with AsyncSessionLocal() as session: stmt = select(User.level, func.count(User.user_id)).group_by(User.level) result = await session.execute(stmt) levels = dict(result.all()) total_stmt = select(func.count(User.user_id)) total = await session.scalar(total_stmt) text = '📊 РАСПРЕДЕЛЕНИЕ ПО УРОВНЯМ\n\n' text += f'Всего пользователей: {total}\n\n' level_order = ['Лидер', 'Активист', 'Сосед', 'Новичок'] level_icons = { 'Лидер': '👑', 'Активист': '⭐', 'Сосед': '🏠', 'Новичок': '🌱' } for level in level_order: count = levels.get(level, 0) icon = level_icons.get(level, '•') percent = round(count / total * 100, 1) if total > 0 else 0 text += f'{icon} {level}: {count} ({percent}%)\n' text += '\nУровни:\n' text += '🌱 Новичок: 0-99 баллов\n' text += '🏠 Сосед: 100-499 баллов\n' text += '⭐ Активист: 500-999 баллов\n' text += '👑 Лидер: 1000+ баллов' await message.answer(text, reply_markup=get_admin_rating_menu(), parse_mode='HTML')