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