domovoy_bot/handlers/admin.py
Admin ba4676b0af v1.0 MVP - Инициализация проекта 🏠
 Регистрация и верификация жильцов
 Рейтинг активности (уровни)
 Детект шпионов (Score 0-100)
 Анти-мат система (3 предупреждения → бан)
 Админ-панель с рассылкой
 Учёт квартир (несколько жильцов)
 Телефоны экстренных служб и мастеров
 Еженедельный экспорт JSON
 Прокси (socks5) для обхода РКН

Бекап: backups/versions/v1.0_mvp_2026-02-26/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-27 10:54:16 +00:00

900 lines
34 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 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(
'⚠️ <b>Админ-панель доступна только в личных сообщениях!</b>\n\n'
'Напишите боту в личку: /admin',
parse_mode='HTML'
)
return
await message.answer(
'🛡️ <b>Панель администратора</b>\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 = '🚨 <b>ШПИОНЫ</b> 🚨\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'👥 <b>Пользователи</b> (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} <code>{user.user_id}</code> — {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 = '''
🛡️ <b>СПРАВКА АДМИНИСТРАТОРА</b>
📋 <b>Основные команды:</b>
<b>📊 Статистика и пользователи:</b>
• /stats — Статистика чата
• /users — Список всех пользователей
• /unverified_list — С отозванной верификацией
<b>🏆 Рейтинг и активность:</b>
• /rating <ID> — Рейтинг пользователя
• /top — Топ-10 по рейтингу
• /rank — Распределение по уровням
<b>🕵️ Безопасность:</b>
• /spies — Список шпионов (score > 60)
• /ban <ID> — Забанить пользователя
• /unban <ID> — Разбанить
• /revoke <ID> — Снять верификацию
• /unverify <ID> — Отозвать (7 дней)
<b>🏠 Квартиры:</b>
• /apt <номер> — Жильцы квартиры
• /apt_stats — Статистика по квартирам
• /find <часть> — Поиск квартиры
<b>⚠️ Предупреждения (анти-мат):</b>
• /warnings — Список предупреждений
• /clear_warnings <ID> — Сбросить
• /add_bad_word — Добавить слово
• /remove_bad_word — Удалить слово
<b>📤 Рассылка и экспорт:</b>
• /admin → кнопка «Рассылка»
• /export — Экспорт JSON
• /check_unverified — Кто не верифицирован >7 дней
<b> Общее:</b>
• /admin — Админ-меню
• /admin_help — Эта справка
• /cancel — Отменить действие
💡 <b>Совет:</b>
Используйте кнопки в меню для быстрого доступа!
'''
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'🕵️ <b>Подозрительные пользователи</b> ({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'👥 <b>Пользователи</b> (последние 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} <code>{user.user_id}</code> — {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'⏳ <b>Непроверенные</b> ({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'✅ <b>Экспорт завершён</b>\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(
'⚠️ <b>Рассылка доступна только в личных сообщениях!</b>\n\n'
'Напишите боту в личку и выберите эту команду там.',
parse_mode='HTML'
)
return
await callback.message.answer(
'📤 <b>Рассылка</b>\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'📤 <b>Начинаю рассылку...</b>\n\n'
f'Сообщение: {broadcast_text[:100]}...'
)
# Отправляем в чат дома
try:
await message.bot.send_message(
config.ADMIN_CHAT_ID,
f'📢 <b>Объявление от администрации</b>\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'📢 <b>Объявление от администрации</b>\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'✅ <b>Рассылка завершена!</b>\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 <user_id>
"""
args = message.text.split()
if len(args) < 2:
await message.answer(
'Использование: <code>/revoke &lt;user_id&gt;</code>\n\n'
'Пример: <code>/revoke 123456789</code>\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'⚠️ <b>Верификация снята</b>\n\n'
f'Администратор снял вашу верификацию.\n\n'
f'❗ <b>Что дальше:</b>\n'
f'Напишите боту номер квартиры ещё раз для повторной верификации.',
parse_mode='HTML'
)
except Exception as e:
logger.error(f'Не удалось уведомить пользователя: {e}')
# Отвечаем админу
await message.answer(
f'✅ <b>Верификация снята</b>\n\n'
f'Пользователь: {user.full_name}\n'
f'Квартира: {user.apartment}\n'
f'ID: <code>{user.user_id}</code>\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 = '🚨 <b>ШПИОНЫ</b> 🚨\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 = '''
🛡️ <b>СПРАВКА АДМИНИСТРАТОРА</b>
📋 <b>Основные команды:</b>
<b>📊 Статистика и пользователи:</b>
• /stats — Статистика чата
• /users — Список всех пользователей
• /unverified_list — С отозванной верификацией
<b>🏆 Рейтинг и активность:</b>
• /rating <ID> — Рейтинг пользователя
• /top — Топ-10 по рейтингу
• /rank — Распределение по уровням
<b>🕵️ Безопасность:</b>
• /spies — Список шпионов (score > 60)
• /ban <ID> — Забанить пользователя
• /unban <ID> — Разбанить
• /revoke <ID> — Снять верификацию
• /unverify <ID> — Отозвать (7 дней)
<b>🏠 Квартиры:</b>
• /apt <номер> — Жильцы квартиры
• /apt_stats — Статистика по квартирам
• /find <часть> — Поиск квартиры
<b>📱 Телефоны:</b>
• /add_phone <ID> <тел> — Добавить телефон
• /edit_phone <ID> <тел> — Изменить телефон
• /remove_phone <ID> — Удалить телефон
• /phone <ID> — Показать телефон
<b>⚠️ Предупреждения (анти-мат):</b>
• /warnings — Список предупреждений
• /clear_warnings <ID> — Сбросить
• /add_bad_word — Добавить слово
• /remove_bad_word — Удалить слово
<b>📤 Рассылка и экспорт:</b>
• /admin → кнопка «Рассылка»
• /export — Экспорт JSON
• /check_unverified — Кто не верифицирован >7 дней
<b> Общее:</b>
• /admin — Админ-меню
• /admin_help — Эта справка
• /cancel — Отменить действие
💡 <b>Совет:</b>
Используйте кнопки в меню для быстрого доступа!
'''
await message.answer(help_text, parse_mode='HTML')
# =============================================================================
# РЕЙТИНГ ПОЛЬЗОВАТЕЛЕЙ
# =============================================================================
@router.message(Command('rating'))
@router.message(Command('рейтинг'))
@admin_only
async def cmd_rating(message: Message):
"""
Показать рейтинг конкретного пользователя
Использование: /rating <ID> или /rating <@username>
"""
# Проверяем что это личка
if message.chat.type != 'private':
await message.answer(
'⚠️ <b>Команда доступна только в личных сообщениях!</b>\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(
'⚠️ <b>Команда доступна только в личных сообщениях!</b>\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 = '🏆 <b>ТОП-10 ПО РЕЙТИНГУ</b>\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} <b>{name}</b>{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(
'⚠️ <b>Команда доступна только в личных сообщениях!</b>\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 = '📊 <b>РАСПРЕДЕЛЕНИЕ ПО УРОВНЯМ</b>\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} <b>{level}</b>: {count} ({percent}%)\n'
text += '\n<b>Уровни:</b>\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'📊 <b>РЕЙТИНГ ПОЛЬЗОВАТЕЛЯ</b>\n\n'
text += f'👤 {name}{apt}\n'
text += f'ID: <code>{user.user_id}</code>\n\n'
text += f'{level_icon} <b>Уровень:</b> {user.level}\n'
text += f'🏆 <b>Рейтинг:</b> {user.rating} баллов\n'
text += f'💬 <b>Сообщений:</b> {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'📈 <b>До следующего уровня:</b> {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 = '🏆 <b>ТОП-10 ПО РЕЙТИНГУ</b>\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} <b>{name}</b>{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 = '🏆 <b>ТОП-10 ПО РЕЙТИНГУ</b>\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} <b>{name}</b>{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_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 = '📊 <b>РАСПРЕДЕЛЕНИЕ ПО УРОВНЯМ</b>\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} <b>{level}</b>: {count} ({percent}%)\n'
text += '\n<b>Уровни:</b>\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')