✅ Кнопки в inline меню админа ✅ Кнопки в reply меню админа ✅ Обработчики callback для кнопок ✅ Исправлено отображение команд 📦 Бекап: backups/versions/v2.1.1_buttons_fix_2026-02-28/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
962 lines
36 KiB
Python
962 lines
36 KiB
Python
"""
|
||
Админские команды
|
||
Только для владельца и доверенных лиц
|
||
"""
|
||
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 <user_id></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_help')
|
||
@admin_only
|
||
async def cb_admin_help(message: CallbackQuery):
|
||
"""Кнопка Помощь"""
|
||
text = (
|
||
'❓ <b>Помощь администратору</b>\n\n'
|
||
'<b>Основные команды:</b>\n'
|
||
'• /admin — Админ-панель\n'
|
||
'• /stats — Статистика чата\n'
|
||
'• /spies — Подозрительные пользователи\n'
|
||
'• /users — Список жильцов\n'
|
||
'• /verify — Заявки на верификацию\n'
|
||
'• /export — Экспорт данных\n'
|
||
'• /rating — Рейтинг пользователей\n'
|
||
'\n'
|
||
'<b>Инициативная группа:</b>\n'
|
||
'• /ig_add — Добавить в ИГ\n'
|
||
'• /ig_remove — Удалить из ИГ\n'
|
||
'• /ig_list — Список ИГ\n'
|
||
'• /ig_broadcast — Рассылка для ИГ\n'
|
||
'\n'
|
||
'<b>События:</b>\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 = (
|
||
'🚫 <b>Отозвать верификацию</b>\n\n'
|
||
'Использование:\n'
|
||
'<code>/unverify <user_id> [причина]</code>\n\n'
|
||
'Пример:\n'
|
||
'<code>/unverify 123456789 Подозрение на шпиона</code>\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(
|
||
'📋 <b>Список не верифицированных</b>\n\n'
|
||
'Использование:\n'
|
||
'<code>/unverified_list</code>\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 = '📊 <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')
|