✅ Теперь кнопка показывает реальный список: - Пользователи с отозванной верификацией - Показывает ID, квартиру, когда отозвано - Сколько дней назад 📝 Было: - Показывала подсказку по команде /unverify 📝 Стало: - Показывает реальный список пользователей - Сразу видно у кого отозвана верификация - Удобно для админа! 💡 Все кнопки теперь работают без подсказок! - Для подсказок есть кнопка '❓ Помощь' - Остальные кнопки показывают результат сразу! Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025 lines
74 KiB
Python
2025 lines
74 KiB
Python
"""
|
||
Админские команды
|
||
Только для владельца и доверенных лиц
|
||
"""
|
||
import logging
|
||
import json
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup
|
||
from aiogram.filters import Command, StateFilter
|
||
from aiogram.fsm.state import State, StatesGroup
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, func, String
|
||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||
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(callback: CallbackQuery):
|
||
"""Кнопка Отозвать верификацию — показывает реальный список"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Показываем пользователей у которых verified = False и была верификация
|
||
stmt = (
|
||
select(User)
|
||
.where(User.verified == False)
|
||
.where(User.verification_date != None)
|
||
.order_by(User.verification_date.desc())
|
||
.limit(20)
|
||
)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await callback.message.answer('✅ Нет пользователей с отозванной верификацией')
|
||
return
|
||
|
||
text = '🚫 <b>ОТОЗВАНА ВЕРИФИКАЦИЯ</b>\n\n'
|
||
text += f'Всего: {len(users)}\n\n'
|
||
|
||
for user in users:
|
||
name = user.full_name or f'user{user.user_id}'
|
||
apt = f' (кв. {user.apartment})' if user.apartment else ''
|
||
days_ago = (datetime.utcnow() - user.verification_date).days if user.verification_date else '?'
|
||
text += f'• {name}{apt}\n'
|
||
text += f' └─ ID: {user.user_id} | отозвано {days_ago} дн. назад\n'
|
||
|
||
text += '\n💡 <b>Что делать:</b>\n'
|
||
text += '• Верифицировать повторно через веб-админку\n'
|
||
text += '• Или оставить как есть (пользователь должен пройти верификацию заново)'
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_unverified_list')
|
||
@admin_only
|
||
async def cb_admin_unverified_list(callback: CallbackQuery):
|
||
"""Кнопка Список не верифицированных — показывает реальный список"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Показываем пользователей у которых verified = False
|
||
stmt = select(User).where(User.verified == False).order_by(User.join_date.desc()).limit(50)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await callback.message.answer('✅ Все пользователи верифицированы')
|
||
return
|
||
|
||
text = '📋 <b>СПИСОК НЕ ВЕРИФИЦИРОВАННЫХ</b>\n\n'
|
||
text += f'Всего: {len(users)}\n\n'
|
||
|
||
for user in users:
|
||
name = user.full_name or f'user{user.user_id}'
|
||
apt = f' (кв. {user.apartment})' if user.apartment else ''
|
||
username = f'@{user.username}' if user.username else ''
|
||
text += f'• {name} {username}{apt}\n'
|
||
text += f' └─ ID: {user.user_id} | в боте с {user.join_date.strftime("%d.%m.%Y") if user.join_date else "?"}\n'
|
||
|
||
text += '\n💡 <b>Что делать:</b>\n'
|
||
text += '• Проверить заявку через /verification\n'
|
||
text += '• Или верифицировать через веб-админку'
|
||
|
||
await callback.message.answer(text, 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')
|
||
|
||
|
||
# =============================================================================
|
||
# НОВЫЕ ОБРАБОТЧИКИ ДЛЯ КНОПОК
|
||
# =============================================================================
|
||
|
||
from keyboards.admin_menu import (
|
||
get_admin_toxicity_menu, get_admin_search_menu, get_admin_antispam_menu,
|
||
get_user_pagination_keyboard, get_user_action_menu, get_bad_words_list_keyboard,
|
||
get_yes_no_keyboard, get_back_keyboard
|
||
)
|
||
from sqlalchemy import desc
|
||
|
||
|
||
# Хранилище страниц для пагинации (user_id -> {page, users})
|
||
_user_pages: dict[int, dict] = {}
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_toxicity')
|
||
@admin_only
|
||
async def cb_admin_toxicity(callback: CallbackQuery):
|
||
"""Кнопка Токсичность"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'☠️ <b>Токсичность пользователей</b>\n\n'
|
||
'Выберите действие:',
|
||
reply_markup=get_admin_toxicity_menu(),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_toxic_top')
|
||
@admin_only
|
||
async def cb_admin_toxic_top(callback: CallbackQuery):
|
||
"""Топ токсичных пользователей"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(User).where(User.toxicity_score > 0).order_by(
|
||
desc(User.toxicity_score)
|
||
).limit(10)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await callback.message.answer('🟢 Токсичных пользователей не обнаружено')
|
||
return
|
||
|
||
text = '☠️ <b>ТОП-10 ТОКСИЧНЫХ</b>\n\n'
|
||
for i, user in enumerate(users, 1):
|
||
name = user.full_name or f'user{user.user_id}'
|
||
apt = f' (кв. {user.apartment})' if user.apartment else ''
|
||
text += f'{i}. <b>{name}</b>{apt}\n'
|
||
text += f' └─ Toxicity Score: {user.toxicity_score}\n'
|
||
|
||
await callback.message.answer(text, reply_markup=get_admin_toxicity_menu(), parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_toxic_stats')
|
||
@admin_only
|
||
async def cb_admin_toxic_stats(callback: CallbackQuery):
|
||
"""Статистика токсичности"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(func.count(User.user_id)).where(User.toxicity_score > 50)
|
||
toxic_count = await session.scalar(stmt)
|
||
|
||
stmt = select(func.count(User.user_id))
|
||
total = await session.scalar(stmt)
|
||
|
||
stmt = select(func.avg(User.toxicity_score))
|
||
avg_score = await session.scalar(stmt)
|
||
|
||
text = '📊 <b>СТАТИСТИКА ТОКСИЧНОСТИ</b>\n\n'
|
||
text += f'Всего пользователей: {total}\n'
|
||
text += f'Токсичных (>50): {toxic_count}\n'
|
||
text += f'Средний score: {round(avg_score or 0, 1)}\n'
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_search')
|
||
@admin_only
|
||
async def cb_admin_search(callback: CallbackQuery):
|
||
"""Кнопка Поиск"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'🔍 <b>Поиск</b>\n\n'
|
||
'Выберите тип поиска:',
|
||
reply_markup=get_admin_search_menu(),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_search_apt')
|
||
@admin_only
|
||
async def cb_admin_search_apt(callback: CallbackQuery):
|
||
"""Поиск квартиры"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'🏠 <b>Поиск квартиры</b>\n\n'
|
||
'Введите номер квартиры для поиска:\n'
|
||
'(или нажмите "Назад" для отмены)',
|
||
reply_markup=get_back_keyboard('admin_search'),
|
||
parse_mode='HTML'
|
||
)
|
||
# Устанавливаем состояние для поиска квартиры
|
||
await callback.bot.set_state(callback.from_user.id, SearchState.waiting_for_apt_search)
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_search_user')
|
||
@admin_only
|
||
async def cb_admin_search_user(callback: CallbackQuery):
|
||
"""Поиск пользователя"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'👤 <b>Поиск пользователя</b>\n\n'
|
||
'Введите имя, username или ID пользователя:\n'
|
||
'(или нажмите "Назад" для отмены)',
|
||
reply_markup=get_back_keyboard('admin_search'),
|
||
parse_mode='HTML'
|
||
)
|
||
# Устанавливаем состояние для поиска пользователя
|
||
await callback.bot.set_state(callback.from_user.id, SearchState.waiting_for_user_search)
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_user_actions')
|
||
@admin_only
|
||
async def cb_admin_user_actions(callback: CallbackQuery):
|
||
"""Действия с пользователем - показываем список с пагинацией"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(User).order_by(desc(User.join_date)).limit(200)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await callback.message.answer('📭 Пользователей нет')
|
||
return
|
||
|
||
# Сохраняем список пользователей для сессии
|
||
_user_pages[callback.from_user.id] = {
|
||
'users': users,
|
||
'page': 0
|
||
}
|
||
|
||
await callback.message.answer(
|
||
'👥 <b>Выберите пользователя</b>\n\n'
|
||
f'Всего: {len(users)} | Страница 1',
|
||
reply_markup=get_user_pagination_keyboard(users, page=0),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('user_action_page_'))
|
||
@admin_only
|
||
async def cb_user_action_page(callback: CallbackQuery):
|
||
"""Переключение страницы в списке пользователей"""
|
||
await callback.answer()
|
||
|
||
page = int(callback.data.split('_')[-1])
|
||
user_data = _user_pages.get(callback.from_user.id)
|
||
|
||
if not user_data:
|
||
await callback.message.answer('❌ Сессия истекла. Выберите "Действия" заново')
|
||
return
|
||
|
||
users = user_data['users']
|
||
user_data['page'] = page
|
||
|
||
await callback.message.edit_reply_markup(
|
||
reply_markup=get_user_pagination_keyboard(users, page=page)
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('user_action_') and F.data.split('_')[-1].isdigit())
|
||
@admin_only
|
||
async def cb_user_action_select(callback: CallbackQuery):
|
||
"""Выбор конкретного пользователя"""
|
||
await callback.answer()
|
||
|
||
user_id = int(callback.data.split('_')[-1])
|
||
|
||
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 callback.message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
name = user.full_name or f'user{user_id}'
|
||
|
||
await callback.message.answer(
|
||
f'⚙️ <b>Действия с пользователем</b>\n\n'
|
||
f'Выберите действие:',
|
||
reply_markup=get_user_action_menu(user_id, name, user.apartment),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('user_ban_'))
|
||
@admin_only
|
||
async def cb_user_ban(callback: CallbackQuery):
|
||
"""Забанить пользователя"""
|
||
await callback.answer()
|
||
user_id = int(callback.data.split('_')[-1])
|
||
|
||
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 callback.message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
name = user.full_name or f'user{user_id}'
|
||
|
||
await callback.message.answer(
|
||
f'🚫 <b>Забанить пользователя?</b>\n\n'
|
||
f'{name} (ID: {user_id})\n\n'
|
||
f'Пользователь не сможет писать в чат.',
|
||
reply_markup=get_yes_no_keyboard(f'ban_confirm_{user_id}'),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('ban_confirm_yes'))
|
||
@admin_only
|
||
async def cb_user_ban_confirm(callback: CallbackQuery):
|
||
"""Подтверждение бана"""
|
||
await callback.answer()
|
||
user_id = int(callback.data.split('_')[-1])
|
||
|
||
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:
|
||
user.banned = True
|
||
await session.commit()
|
||
|
||
name = user.full_name or f'user{user_id}' if user else 'Пользователь'
|
||
|
||
await callback.message.answer(f'✅ <b>Забанен</b>\n\n{name} (ID: {user_id})', parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data.startswith('ban_confirm_no'))
|
||
@admin_only
|
||
async def cb_user_ban_cancel(callback: CallbackQuery):
|
||
"""Отмена бана"""
|
||
await callback.answer('Отменено')
|
||
await callback.message.answer('❌ Отменено')
|
||
|
||
|
||
@router.callback_query(F.data.startswith('user_unban_'))
|
||
@admin_only
|
||
async def cb_user_unban(callback: CallbackQuery):
|
||
"""Разбанить пользователя"""
|
||
await callback.answer()
|
||
user_id = int(callback.data.split('_')[-1])
|
||
|
||
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 callback.message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
if not user.banned:
|
||
await callback.message.answer('ℹ️ Пользователь не забанен')
|
||
return
|
||
|
||
user.banned = False
|
||
await session.commit()
|
||
|
||
name = user.full_name or f'user{user_id}'
|
||
await callback.message.answer(f'✅ <b>Разбанен</b>\n\n{name} (ID: {user_id})', parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data.startswith('user_revoke_'))
|
||
@admin_only
|
||
async def cb_user_revoke(callback: CallbackQuery):
|
||
"""Снять верификацию"""
|
||
await callback.answer()
|
||
user_id = int(callback.data.split('_')[-1])
|
||
|
||
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 callback.message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
if user.is_admin:
|
||
await callback.message.answer('❌ Нельзя снять верификацию с администратора!')
|
||
return
|
||
|
||
if not user.verified:
|
||
await callback.message.answer('ℹ️ Пользователь уже не верифицирован')
|
||
return
|
||
|
||
user.verified = False
|
||
await session.commit()
|
||
|
||
name = user.full_name or f'user{user_id}'
|
||
await callback.message.answer(
|
||
f'✅ <b>Верификация снята</b>\n\n{name} (ID: {user_id})\n'
|
||
f'Теперь пользователь должен пройти верификацию заново.',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('user_unverify_'))
|
||
@admin_only
|
||
async def cb_user_unverify(callback: CallbackQuery):
|
||
"""Отозвать верификацию на 7 дней"""
|
||
await callback.answer()
|
||
user_id = int(callback.data.split('_')[-1])
|
||
|
||
# TODO: Реализовать отзыв на 7 дней (требуется поле в БД)
|
||
await callback.message.answer(
|
||
'⏳ <b>Функция в разработке</b>\n\n'
|
||
'Используйте команду /unverify для отзыва на 7 дней',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('user_rating_'))
|
||
@admin_only
|
||
async def cb_user_rating(callback: CallbackQuery):
|
||
"""Показать рейтинг пользователя"""
|
||
await callback.answer()
|
||
user_id = int(callback.data.split('_')[-1])
|
||
|
||
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 callback.message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
from utils.formatters import format_rating_user
|
||
text = format_rating_user(user)
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data.startswith('user_history_'))
|
||
@admin_only
|
||
async def cb_user_history(callback: CallbackQuery):
|
||
"""Показать историю пользователя"""
|
||
await callback.answer()
|
||
user_id = int(callback.data.split('_')[-1])
|
||
|
||
# TODO: Реализовать показ истории профиля
|
||
await callback.message.answer(
|
||
'📝 <b>История профиля</b>\n\n'
|
||
f'ID: {user_id}\n\n'
|
||
'Используйте команду /history {user_id} для просмотра',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('user_phone_'))
|
||
@admin_only
|
||
async def cb_user_phone(callback: CallbackQuery):
|
||
"""Показать телефон пользователя"""
|
||
await callback.answer()
|
||
user_id = int(callback.data.split('_')[-1])
|
||
|
||
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 callback.message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
phone = user.phone or '📭 Не указан'
|
||
name = user.full_name or f'user{user_id}'
|
||
|
||
await callback.message.answer(
|
||
f'📞 <b>Телефон</b>\n\n'
|
||
f'{name}: <code>{phone}</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_muted')
|
||
@admin_only
|
||
async def cb_admin_muted(callback: CallbackQuery):
|
||
"""Список замутованных"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
from database.models import RateLimit
|
||
from datetime import datetime
|
||
|
||
now = datetime.utcnow()
|
||
stmt = select(User).join(RateLimit).where(
|
||
RateLimit.mute_until > now
|
||
)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await callback.message.answer('🟢 Замутованных пользователей нет')
|
||
return
|
||
|
||
text = '🔇 <b>ЗАМУТОВАННЫЕ</b>\n\n'
|
||
for user in users:
|
||
name = user.full_name or f'user{user.user_id}'
|
||
apt = f' (кв. {user.apartment})' if user.apartment else ''
|
||
text += f'• {name}{apt} (ID: {user.user_id})\n'
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_banned')
|
||
@admin_only
|
||
async def cb_admin_banned(callback: CallbackQuery):
|
||
"""Список забаненных"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(User).where(User.banned == True)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await callback.message.answer('🟢 Забаненных пользователей нет')
|
||
return
|
||
|
||
text = '🚫 <b>ЗАБАНЕННЫЕ</b>\n\n'
|
||
for user in users:
|
||
name = user.full_name or f'user{user.user_id}'
|
||
apt = f' (кв. {user.apartment})' if user.apartment else ''
|
||
text += f'• {name}{apt} (ID: {user.user_id})\n'
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_apartments')
|
||
@admin_only
|
||
async def cb_admin_apartments(callback: CallbackQuery):
|
||
"""Меню квартир"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'🏠 <b>Квартиры</b>\n\n'
|
||
'Выберите действие:',
|
||
reply_markup=get_admin_apartments_menu(),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_apt_stats')
|
||
@admin_only
|
||
async def cb_admin_apt_stats(callback: CallbackQuery):
|
||
"""Статистика квартир"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Считаем сколько квартир и жильцов
|
||
from sqlalchemy import func
|
||
stmt = select(func.count(func.distinct(User.apartment)))
|
||
apt_count = await session.scalar(stmt)
|
||
|
||
stmt = select(func.count(User.user_id))
|
||
user_count = await session.scalar(stmt)
|
||
|
||
# Топ-5 квартир по количеству жильцов
|
||
stmt = select(User.apartment, func.count(User.user_id)).group_by(User.apartment).order_by(
|
||
func.count(User.user_id).desc()
|
||
).limit(5)
|
||
result = await session.execute(stmt)
|
||
top_apts = result.all()
|
||
|
||
text = '📊 <b>СТАТИСТИКА КВАРТИР</b>\n\n'
|
||
text += f'Всего квартир: {apt_count}\n'
|
||
text += f'Всего жильцов: {user_count}\n\n'
|
||
text += '<b>Топ-5 по жильцам:</b>\n'
|
||
for apt, count in top_apts:
|
||
text += f'• кв. {apt}: {count} чел.\n'
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_warn_list')
|
||
@admin_only
|
||
async def cb_admin_warn_list(callback: CallbackQuery):
|
||
"""Список предупреждений (анти-мат)"""
|
||
await callback.answer()
|
||
|
||
# Загружаем стоп-слова
|
||
from handlers.antimat import load_bad_words
|
||
words = load_bad_words()
|
||
|
||
text = f'📋 <b>Стоп-слова</b> ({len(words)}):\n\n'
|
||
text += 'Используйте кнопки для управления:\n'
|
||
|
||
await callback.message.answer(text, reply_markup=get_bad_words_list_keyboard(words), parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_warnings')
|
||
@admin_only
|
||
async def cb_admin_warnings_menu(callback: CallbackQuery):
|
||
"""Меню предупреждений"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'⚠️ <b>Предупреждения и анти-спам</b>\n\n'
|
||
'Выберите раздел:',
|
||
reply_markup=get_admin_antispam_menu(),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_bad_words')
|
||
@admin_only
|
||
async def cb_admin_bad_words(callback: CallbackQuery):
|
||
"""Список стоп-слов"""
|
||
await callback.answer()
|
||
|
||
# Загружаем стоп-слова из файла
|
||
from handlers.antimat import load_bad_words
|
||
words = load_bad_words()
|
||
|
||
await callback.message.answer(
|
||
f'📋 <b>Стоп-слова</b> ({len(words)}):\n\n'
|
||
'Нажмите на слово чтобы удалить:',
|
||
reply_markup=get_bad_words_list_keyboard(words),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('badword_del_'))
|
||
@admin_only
|
||
async def cb_badword_del(callback: CallbackQuery):
|
||
"""Удаление стоп-слова"""
|
||
await callback.answer()
|
||
|
||
word = callback.data.replace('badword_del_', '')
|
||
|
||
from handlers.antimat import load_bad_words
|
||
import json
|
||
|
||
words = load_bad_words()
|
||
|
||
if word in words:
|
||
words.remove(word)
|
||
|
||
# Сохраняем обратно в файл
|
||
try:
|
||
with open('data/bad_words.json', 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
data['words'] = words
|
||
with open('data/bad_words.json', 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
|
||
await callback.message.answer(f'✅ Слово <b>"{word}"</b> удалено', parse_mode='HTML')
|
||
except Exception as e:
|
||
logger.error(f'Ошибка при удалении слова: {e}')
|
||
await callback.message.answer(f'❌ Ошибка при удалении: {e}')
|
||
else:
|
||
await callback.message.answer(f'ℹ️ Слова "{word}" нет в списке')
|
||
|
||
|
||
@router.callback_query(F.data == 'badword_add')
|
||
@admin_only
|
||
async def cb_badword_add(callback: CallbackQuery):
|
||
"""Добавление стоп-слова"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'➕ <b>Добавить стоп-слово</b>\n\n'
|
||
'Введите слово которое нужно добавить в чёрный список:\n'
|
||
'(или нажмите "Назад" для отмены)',
|
||
reply_markup=get_back_keyboard('admin_bad_words'),
|
||
parse_mode='HTML'
|
||
)
|
||
await callback.bot.set_state(callback.from_user.id, AddWordState.waiting_for_word)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('badwords_page_'))
|
||
@admin_only
|
||
async def cb_badwords_page(callback: CallbackQuery):
|
||
"""Переключение страницы стоп-слов"""
|
||
await callback.answer()
|
||
|
||
page = int(callback.data.split('_')[-1])
|
||
|
||
from handlers.antimat import load_bad_words
|
||
words = load_bad_words()
|
||
|
||
await callback.message.edit_reply_markup(
|
||
reply_markup=get_bad_words_list_keyboard(words, page=page)
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# FSM ДЛЯ ДОБАВЛЕНИЯ СЛОВ
|
||
# =============================================================================
|
||
|
||
|
||
class AddWordState(StatesGroup):
|
||
"""Состояния для добавления слова"""
|
||
waiting_for_word = State()
|
||
|
||
|
||
@router.message(AddWordState.waiting_for_word)
|
||
@admin_only
|
||
async def process_add_word(message: Message):
|
||
"""Обработка добавления слова"""
|
||
word = message.text.strip().lower()
|
||
|
||
from handlers.antimat import load_bad_words
|
||
import json
|
||
|
||
words = load_bad_words()
|
||
|
||
if word in words:
|
||
await message.answer(f'ℹ️ Слово "{word}" уже есть в списке')
|
||
await message.bot.delete_state(message.from_user.id)
|
||
return
|
||
|
||
words.append(word)
|
||
|
||
try:
|
||
with open('data/bad_words.json', 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
data['words'] = words
|
||
with open('data/bad_words.json', 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
|
||
await message.answer(
|
||
f'✅ Слово <b>"{word}"</b> добавлено\n\n'
|
||
f'Всего слов: {len(words)}',
|
||
reply_markup=get_back_keyboard('admin_bad_words'),
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Ошибка при добавлении слова: {e}')
|
||
await message.answer(f'❌ Ошибка при добавлении: {e}')
|
||
|
||
await message.bot.delete_state(message.from_user.id)
|
||
|
||
|
||
# =============================================================================
|
||
# ОБРАБОТКА ДОМЕНОВ И ПАТТЕРНОВ
|
||
# =============================================================================
|
||
|
||
def load_bad_domains_data() -> dict:
|
||
"""Загрузка данных чёрного списка доменов"""
|
||
try:
|
||
with open('data/bad_domains.json', 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
except:
|
||
return {'domains': [], 'patterns': [], 'keywords': []}
|
||
|
||
|
||
def save_bad_domains_data(data: dict):
|
||
"""Сохранение данных чёрного списка доменов"""
|
||
with open('data/bad_domains.json', 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
|
||
|
||
def get_domains_list_keyboard(domains: list, page: int = 0, page_size: int = 20) -> InlineKeyboardMarkup:
|
||
"""Клавиатура со списком доменов"""
|
||
builder = InlineKeyboardBuilder()
|
||
|
||
total_pages = (len(domains) + page_size - 1) // page_size if domains else 1
|
||
start_idx = page * page_size
|
||
end_idx = min(start_idx + page_size, len(domains))
|
||
page_domains = domains[start_idx:end_idx]
|
||
|
||
for domain in page_domains:
|
||
builder.button(
|
||
text=f'❌ {domain}',
|
||
callback_data=f'baddomain_del_{domain}'
|
||
)
|
||
|
||
builder.row(
|
||
InlineKeyboardButton(text='➕ Добавить домен', callback_data='baddomain_add'),
|
||
)
|
||
|
||
nav_buttons = []
|
||
if page > 0:
|
||
nav_buttons.append(InlineKeyboardButton(text='⬅️', callback_data=f'baddomains_page_{page - 1}'))
|
||
if page < total_pages - 1:
|
||
nav_buttons.append(InlineKeyboardButton(text='➡️', callback_data=f'baddomains_page_{page + 1}'))
|
||
|
||
if nav_buttons:
|
||
builder.row(*nav_buttons)
|
||
|
||
builder.row(
|
||
InlineKeyboardButton(text='🔙 Назад', callback_data='admin_warnings'),
|
||
)
|
||
|
||
return builder.as_markup()
|
||
|
||
|
||
def get_patterns_list_keyboard(patterns: list, page: int = 0, page_size: int = 20) -> InlineKeyboardMarkup:
|
||
"""Клавиатура со списком паттернов"""
|
||
builder = InlineKeyboardBuilder()
|
||
|
||
total_pages = (len(patterns) + page_size - 1) // page_size if patterns else 1
|
||
start_idx = page * page_size
|
||
end_idx = min(start_idx + page_size, len(patterns))
|
||
page_patterns = patterns[start_idx:end_idx]
|
||
|
||
for pattern in page_patterns:
|
||
builder.button(
|
||
text=f'❌ {pattern}',
|
||
callback_data=f'badpattern_del_{pattern}'
|
||
)
|
||
|
||
builder.row(
|
||
InlineKeyboardButton(text='➕ Добавить паттерн', callback_data='badpattern_add'),
|
||
)
|
||
|
||
nav_buttons = []
|
||
if page > 0:
|
||
nav_buttons.append(InlineKeyboardButton(text='⬅️', callback_data=f'badpatterns_page_{page - 1}'))
|
||
if page < total_pages - 1:
|
||
nav_buttons.append(InlineKeyboardButton(text='➡️', callback_data=f'badpatterns_page_{page + 1}'))
|
||
|
||
if nav_buttons:
|
||
builder.row(*nav_buttons)
|
||
|
||
builder.row(
|
||
InlineKeyboardButton(text='🔙 Назад', callback_data='admin_warnings'),
|
||
)
|
||
|
||
return builder.as_markup()
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_bad_domains')
|
||
@admin_only
|
||
async def cb_admin_bad_domains(callback: CallbackQuery):
|
||
"""Список запрещённых доменов"""
|
||
await callback.answer()
|
||
|
||
data = load_bad_domains_data()
|
||
domains = data.get('domains', [])
|
||
|
||
await callback.message.answer(
|
||
f'🌐 <b>Запрещённые домены</b> ({len(domains)}):\n\n'
|
||
'Нажмите на домен чтобы удалить:',
|
||
reply_markup=get_domains_list_keyboard(domains),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == 'admin_bad_patterns')
|
||
@admin_only
|
||
async def cb_admin_bad_patterns(callback: CallbackQuery):
|
||
"""Список запрещённых паттернов"""
|
||
await callback.answer()
|
||
|
||
data = load_bad_domains_data()
|
||
patterns = data.get('patterns', [])
|
||
|
||
await callback.message.answer(
|
||
f'🔑 <b>Запрещённые паттерны</b> ({len(patterns)}):\n\n'
|
||
'Нажмите на паттерн чтобы удалить:',
|
||
reply_markup=get_patterns_list_keyboard(patterns),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('baddomain_del_'))
|
||
@admin_only
|
||
async def cb_baddomain_del(callback: CallbackQuery):
|
||
"""Удаление домена"""
|
||
await callback.answer()
|
||
|
||
domain = callback.data.replace('baddomain_del_', '')
|
||
data = load_bad_domains_data()
|
||
|
||
if domain in data.get('domains', []):
|
||
data['domains'].remove(domain)
|
||
save_bad_domains_data(data)
|
||
await callback.message.answer(f'✅ Домен <b>"{domain}"</b> удалён', parse_mode='HTML')
|
||
else:
|
||
await callback.message.answer(f'ℹ️ Домена "{domain}" нет в списке')
|
||
|
||
|
||
@router.callback_query(F.data.startswith('badpattern_del_'))
|
||
@admin_only
|
||
async def cb_badpattern_del(callback: CallbackQuery):
|
||
"""Удаление паттерна"""
|
||
await callback.answer()
|
||
|
||
pattern = callback.data.replace('badpattern_del_', '')
|
||
data = load_bad_domains_data()
|
||
|
||
if pattern in data.get('patterns', []):
|
||
data['patterns'].remove(pattern)
|
||
save_bad_domains_data(data)
|
||
await callback.message.answer(f'✅ Паттерн <b>"{pattern}"</b> удалён', parse_mode='HTML')
|
||
else:
|
||
await callback.message.answer(f'ℹ️ Паттерна "{pattern}" нет в списке')
|
||
|
||
|
||
@router.callback_query(F.data == 'baddomain_add')
|
||
@admin_only
|
||
async def cb_baddomain_add(callback: CallbackQuery):
|
||
"""Добавление домена"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'🌐 <b>Добавить домен</b>\n\n'
|
||
'Введите домен который нужно добавить в чёрный список:\n'
|
||
'(или нажмите "Назад" для отмены)',
|
||
reply_markup=get_back_keyboard('admin_bad_domains'),
|
||
parse_mode='HTML'
|
||
)
|
||
await callback.bot.set_state(callback.from_user.id, AddDomainState.waiting_for_domain)
|
||
|
||
|
||
@router.callback_query(F.data == 'badpattern_add')
|
||
@admin_only
|
||
async def cb_badpattern_add(callback: CallbackQuery):
|
||
"""Добавление паттерна"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'🔑 <b>Добавить паттерн</b>\n\n'
|
||
'Введите паттерн который нужно добавить в чёрный список:\n'
|
||
'(или нажмите "Назад" для отмены)',
|
||
reply_markup=get_back_keyboard('admin_bad_patterns'),
|
||
parse_mode='HTML'
|
||
)
|
||
await callback.bot.set_state(callback.from_user.id, AddPatternState.waiting_for_pattern)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('baddomains_page_'))
|
||
@admin_only
|
||
async def cb_baddomains_page(callback: CallbackQuery):
|
||
"""Переключение страницы доменов"""
|
||
await callback.answer()
|
||
|
||
page = int(callback.data.split('_')[-1])
|
||
data = load_bad_domains_data()
|
||
domains = data.get('domains', [])
|
||
|
||
await callback.message.edit_reply_markup(
|
||
reply_markup=get_domains_list_keyboard(domains, page=page)
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('badpatterns_page_'))
|
||
@admin_only
|
||
async def cb_badpatterns_page(callback: CallbackQuery):
|
||
"""Переключение страницы паттернов"""
|
||
await callback.answer()
|
||
|
||
page = int(callback.data.split('_')[-1])
|
||
data = load_bad_domains_data()
|
||
patterns = data.get('patterns', [])
|
||
|
||
await callback.message.edit_reply_markup(
|
||
reply_markup=get_patterns_list_keyboard(patterns, page=page)
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# FSM ДЛЯ ДОБАВЛЕНИЯ ДОМЕНОВ И ПАТТЕРНОВ
|
||
# =============================================================================
|
||
|
||
|
||
class AddDomainState(StatesGroup):
|
||
"""Состояния для добавления домена"""
|
||
waiting_for_domain = State()
|
||
|
||
|
||
class AddPatternState(StatesGroup):
|
||
"""Состояния для добавления паттерна"""
|
||
waiting_for_pattern = State()
|
||
|
||
|
||
@router.message(AddDomainState.waiting_for_domain)
|
||
@admin_only
|
||
async def process_add_domain(message: Message):
|
||
"""Обработка добавления домена"""
|
||
domain = message.text.strip().lower()
|
||
|
||
data = load_bad_domains_data()
|
||
|
||
if domain in data.get('domains', []):
|
||
await message.answer(f'ℹ️ Домен "{domain}" уже есть в списке')
|
||
await message.bot.delete_state(message.from_user.id)
|
||
return
|
||
|
||
data['domains'].append(domain)
|
||
|
||
try:
|
||
save_bad_domains_data(data)
|
||
await message.answer(
|
||
f'✅ Домен <b>"{domain}"</b> добавлен\n\n'
|
||
f'Всего доменов: {len(data["domains"])}',
|
||
reply_markup=get_back_keyboard('admin_bad_domains'),
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Ошибка при добавлении домена: {e}')
|
||
await message.answer(f'❌ Ошибка при добавлении: {e}')
|
||
|
||
await message.bot.delete_state(message.from_user.id)
|
||
|
||
|
||
@router.message(AddPatternState.waiting_for_pattern)
|
||
@admin_only
|
||
async def process_add_pattern(message: Message):
|
||
"""Обработка добавления паттерна"""
|
||
pattern = message.text.strip()
|
||
|
||
data = load_bad_domains_data()
|
||
|
||
if pattern in data.get('patterns', []):
|
||
await message.answer(f'ℹ️ Паттерн "{pattern}" уже есть в списке')
|
||
await message.bot.delete_state(message.from_user.id)
|
||
return
|
||
|
||
data['patterns'].append(pattern)
|
||
|
||
try:
|
||
save_bad_domains_data(data)
|
||
await message.answer(
|
||
f'✅ Паттерн <b>"{pattern}"</b> добавлен\n\n'
|
||
f'Всего паттернов: {len(data["patterns"])}',
|
||
reply_markup=get_back_keyboard('admin_bad_patterns'),
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Ошибка при добавлении паттерна: {e}')
|
||
await message.answer(f'❌ Ошибка при добавлении: {e}')
|
||
|
||
await message.bot.delete_state(message.from_user.id)
|
||
|
||
|
||
# =============================================================================
|
||
# FSM ДЛЯ ПОИСКА И ВВОДА ПАРАМЕТРОВ
|
||
# =============================================================================
|
||
|
||
|
||
class SearchState(StatesGroup):
|
||
"""Состояния для поиска"""
|
||
waiting_for_apt_search = State()
|
||
waiting_for_user_search = State()
|
||
|
||
|
||
@router.message(SearchState.waiting_for_apt_search)
|
||
@admin_only
|
||
async def process_apt_search(message: Message):
|
||
"""Обработка поиска квартиры"""
|
||
search_query = message.text.strip()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(User).where(User.apartment.like(f'%{search_query}%'))
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await message.answer(f'❌ Квартиры с номером "{search_query}" не найдены')
|
||
await message.bot.delete_state(message.from_user.id)
|
||
return
|
||
|
||
text = f'🏠 <b>Квартира {search_query}</b>\n\n'
|
||
for user in users:
|
||
name = user.full_name or f'user{user.user_id}'
|
||
status = '✅' if user.verified else '⏳'
|
||
text += f'{status} {name} | ID: {user.user_id}\n'
|
||
|
||
await message.answer(text, reply_markup=get_back_keyboard('admin_search'), parse_mode='HTML')
|
||
await message.bot.delete_state(message.from_user.id)
|
||
|
||
|
||
@router.message(SearchState.waiting_for_user_search)
|
||
@admin_only
|
||
async def process_user_search(message: Message):
|
||
"""Обработка поиска пользователя"""
|
||
search_query = message.text.strip()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(User).where(
|
||
(User.full_name.like(f'%{search_query}%')) |
|
||
(User.username.like(f'%{search_query}%')) |
|
||
(User.user_id.cast(String).like(f'%{search_query}%'))
|
||
)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await message.answer(f'❌ Пользователи с "{search_query}" не найдены')
|
||
await message.bot.delete_state(message.from_user.id)
|
||
return
|
||
|
||
text = f'👤 <b>Поиск "{search_query}"</b> ({len(users)}):\n\n'
|
||
for user in users[:20]:
|
||
name = user.full_name or f'user{user.user_id}'
|
||
status = '✅' if user.verified else '⏳'
|
||
apt = f' (кв. {user.apartment})' if user.apartment else ''
|
||
text += f'{status} {name}{apt} | ID: {user.user_id}\n'
|
||
|
||
if len(users) > 20:
|
||
text += f'\n... и ещё {len(users) - 20}'
|
||
|
||
await message.answer(text, reply_markup=get_back_keyboard('admin_search'), parse_mode='HTML')
|
||
await message.bot.delete_state(message.from_user.id)
|